/*
 * Ext Gantt 1.2
 * Copyright(c) 2009-2011 Mats Bryntse Consulting
 * 
 * info@ext-scheduler.com
 * 
 * http://www.ext-scheduler.com/license.html
 * 
 */



// Override to be able to track previous value for record fields
Ext.override(Ext.data.Record, {
    set : function(name, value){
        var encode = Ext.isPrimitive(value) ? String : Ext.encode;
        if(encode(this.data[name]) == encode(value)) {
            return;
        }        
        this.dirty = true;
        
        // Keep previous Field value
        if(!this.previous){
            this.previous = {};
        }
        this.previous[name] = this.data[name];

        if(!this.modified){
            this.modified = {};
        }
        if(this.modified[name] === undefined){
            this.modified[name] = this.data[name];
        }

        this.data[name] = value;
        if(!this.editing){
            this.afterEdit();
        }
    },
    
    afterEdit: function(){
        if(this.store){
            this.store.afterEdit(this, this.previous);
        }
        
        // Don't need this anymore
        delete this.previous;
    },
    
    reject : function(silent){
        var m = this.modified,
            current = {};
        for(var n in m){
            if(typeof m[n] != "function"){
                // Save current value
                current[n] = this.data[n];
                this.data[n] = m[n];
            }
        }
        delete this.modified;
        this.dirty = false;
        this.editing = false;
        if(silent !== true){
            this.afterReject(current);
        }
    },
    
    // private
    afterReject: function(current){
        if(this.store){
            this.store.afterReject(this, current);
        }
    }
});

Ext.override(Ext.data.Store,{
    afterEdit : function(record, hashPrevious){
        if(this.modified.indexOf(record) == -1){
            this.modified.push(record);
        }
        this.fireEvent('update', this, record, Ext.data.Record.EDIT, hashPrevious);
    },

    // private
    afterReject : function(record, hashPrevious){
        this.modified.remove(record);
        this.fireEvent('update', this, record, Ext.data.Record.REJECT, hashPrevious);
    }
});

// Temp fix for: http://www.extjs.com/forum/showthread.php?98659-3.2.1-Ext.dd.DragZone-doesn-t-clean-up-properly-when-using-containerScroll-true&p=465198#post465198
Ext.override(Ext.dd.DragZone, {
    destroy : Ext.dd.DragZone.prototype.destroy.createInterceptor(function() {
        if (this.containerScroll) {
            Ext.dd.ScrollManager.unregister(this.el);
        }
    })
});


// Some convenience methods, credit https://www.sencha.com/forum/showthread.php?81507-Get-Ext-version-number-in-more-easily-comparable-manner
Ext.getMajorVersion = function(){
      return Math.abs(this.version.split('.')[0]);
};
Ext.getMinorVersion = function(){
      return Math.abs(this.version.split('.')[1]);
};
Ext.getRevision = function(){
      return Math.abs(this.version.split('.')[2]);
};


Ext.ns('Sch.util');

 
Sch.util.Date = {
    
    betweenLesser : function(date, start, end) {
        var t = date.getTime();
        return start.getTime() <= t && t < end.getTime();
    },

    
    constrain : function(date, min, max) {
        return Sch.util.Date.min(Sch.util.Date.max(date, min), max);
    },
    
    
    add : function(date, unit, value) {
        var d = date.clone();
        if (!unit || value === 0) return d;

        switch(unit.toLowerCase()) {
            case this.MILLI:
                d = new Date(date.getTime() + value);
                break;
            case this.SECOND:
                d = new Date(date.getTime() + (value*1000));
                break;
            case this.MINUTE:
                d = new Date(date.getTime() + (value*60000));
                break;
            case this.HOUR:
                d = new Date(date.getTime() + (value*3600000));
                break;
            case this.DAY:
                d.setDate(date.getDate() + value);
                break;
            case this.WEEK:
                d.setDate(date.getDate() + value * 7);
                break;
            case this.MONTH:
                var day = date.getDate();
                if (day > 28) {
                    day = Math.min(day, this.add(date.getFirstDateOfMonth(), this.MONTH, value).getLastDateOfMonth().getDate());
                }
                d.setDate(day);
                d.setMonth(d.getMonth() + value);
                break;
            case this.QUARTER:
                d = this.add(date, this.MONTH, 3);
                break;
            case this.YEAR:
                d.setFullYear(date.getFullYear() + value);
                break;
        }
        return d;
    },

    getMeasuringUnit : function(unit) {
        switch(unit) {
            case this.WEEK:
                return Sch.util.Date.DAY;
            break;

            default:
                return unit;
            break;
        }
    },

    getDurationInUnit : function(start, end, unit) {
        var units;

        switch(unit) {
            case this.QUARTER:
                units = Math.round(Sch.util.Date.getDurationInMonths(start, end)/3);
            break;

            case this.MONTH:
                units = Math.round(Sch.util.Date.getDurationInMonths(start, end));
            break;

            case this.WEEK:
                units = Math.round(Sch.util.Date.getDurationInDays(start, end))/7;
            break;

            case this.DAY:
                units = Math.round(Sch.util.Date.getDurationInDays(start, end));
            break;

            case this.HOUR:
                units = Math.round(Sch.util.Date.getDurationInHours(start, end));
            break;

            case this.MINUTE:
                units = Math.round(Sch.util.Date.getDurationInMinutes(start, end));
            break;

            case this.SECOND:
                units = Math.round(Sch.util.Date.getDurationInSeconds(start, end));
            break;

            case this.MILLI:
                units = Math.round(Sch.util.Date.getDurationInMilliseconds(start, end));
            break;
        }

        return units;
    },

    getUnitToBaseUnitRatio : function(baseUnit, unit) {
        if (baseUnit === unit) {
            return 1;
        }

        switch (baseUnit) {
            case this.YEAR:
                switch(unit) {
                    case this.QUARTER:
                        return 4;
                    break;

                    case this.MONTH:
                        return 12;
                    break;
                }
            break;

            case this.QUARTER:
                switch(unit) {
                    case this.YEAR:
                        return 1/4;
                    break;

                    case this.MONTH:
                        return 3;
                    break;
                }
            break;

            case this.MONTH:
                switch(unit) {
                    case this.YEAR:
                        return 12;
                    break;

                    case this.QUARTER:
                        return 3;
                    break;
                }
            break;

            case this.WEEK:
                switch(unit) {
                    case this.DAY:
                        return 1/7;
                    break;

                    case this.HOUR:
                        return 1/168;
                    break;
                }
            break;

            case this.DAY:
                switch(unit) {   
                    case this.WEEK:
                        return 7;
                    break;

                    case this.HOUR:
                        return 1/24;
                    break;

                    case this.MINUTE:
                        return 1/1440;
                    break;
                }
            break;

            case this.HOUR:
                switch(unit) {
                    case this.DAY:
                        return 24;
                    break;

                    case this.MINUTE:
                        return 1/60;
                    break;
                }
            break;

            case this.MINUTE:
                switch(unit) {
                    case this.HOUR:
                        return 60;
                    break;

                    case this.SECOND:
                        return 1/60;
                    break;

                    case this.MILLI:
                        return 1/60000;
                    break;
                }
            break;

            case this.SECOND:
                switch(unit) {
                    case this.MILLI:
                        return 1/1000;
                    break;
                }
            break;
        }

        return -1;
    },

    
    getDurationInMilliseconds : function (start, end) {
        return (end - start);
    },

    
    getDurationInSeconds : function (start, end) {
        return (end - start) / 1000;
    },

    
    getDurationInMinutes : function (start, end) {
        return (end - start) / 60000;
    },
    
    
    getDurationInHours : function (start, end) {
        return (end - start) / 3600000;
    },
    
    
    getDurationInDays : function (start, end) {
        return (end - start) / 86400000;
    },
    
    
    getDurationInBusinessDays : function (start, end) {
        var nbrDays = Math.round((end - start) / 86400000),
            nbrBusinessDays = 0,
            d;
        
        for (var i = 0; i < nbrDays; i++) {
            d = Sch.util.Date.add(start, Sch.util.Date.DAY, i).getDay();
            if (d !== 6 && d !== 0) {
                nbrBusinessDays++;
            }
        }
        return nbrBusinessDays;
    },
    
    
    getDurationInMonths : function (start, end) {
        return ((end.getFullYear() - start.getFullYear()) * 12) + (end.getMonth() - start.getMonth());
    },

    
    getDurationInYears : function (start, end) {
        return this.getDurationInMonths(start, end)/12;
    },
    
    
    min : function (d1, d2) {
        return d1 < d2 ? d1 : d2;
    },
    
    
    max : function (d1, d2) {
        return d1 > d2 ? d1 : d2;
    },
    
    
    intersectSpans : function (date1Start, date1End, date2Start, date2End) {
        return this.betweenLesser(date1Start, date2Start, date2End) || 
               this.betweenLesser(date2Start, date1Start, date1End);
    },
    
    
    MILLI : Date.MILLI,

    
    SECOND : Date.SECOND,

    
    MINUTE : Date.MINUTE,

    
    HOUR : Date.HOUR,

    
    DAY : Date.DAY,
    
    
    WEEK : "w",
    
    
    MONTH : Date.MONTH,
    
    
    QUARTER : "q",

    
    YEAR : Date.YEAR
};


Ext.apply(Sch.util.Date, {
    // Returns 1 if first param is a greater unit than second param, -1 if the opposite is true or 0 if they're equal
    compareUnits : (function() {
        var D = Sch.util.Date,
            units = [D.MILLI, D.SECOND, D.MINUTE, D.HOUR, D.DAY, D.WEEK, D.MONTH, D.QUARTER, D.YEAR],
            ind1, ind2;
        
        return function(u1, u2) {
            ind1 = units.indexOf(u1);
            ind2 = units.indexOf(u2);
            
            return ind1 > ind2 ? 1 : (ind1 < ind2 ? -1 : 0); 
        };
    })()
});


 
Ext.ns('Sch');


Sch.LazyResizable = function(el, config, direction, e){
    this.addEvents(
         
        'partialresize'
    );
    
    Sch.LazyResizable.superclass.constructor.apply(this, arguments);
    
    // Start the resizing instantly
    this.handleOver();
    this.onMouseDown(this[direction], e);
};

Ext.extend(Sch.LazyResizable, Ext.Resizable, {
    
    // Override to include beforeresize firing to possibly stop the execution
    startSizing : Ext.Resizable.prototype.startSizing.createInterceptor(function(e) {
        return this.fireEvent('beforeresize', this, e) !== false;
    }),
    
    // Override to include partialresize firing
    resizeElement : Ext.Resizable.prototype.resizeElement.createInterceptor(function(){ 
        var box = this.proxy.getBox(),
            oldWidth = this.el.getWidth();
        
        if (box.width !== oldWidth) {
            this.fireEvent('partialresize', this, box.width, oldWidth);
        }
    })
        
});




Ext.ns('Sch');


Sch.AbstractSchedulerView = Ext.extend(Ext.grid.GridView, {
    // Increase depth for getTarget functionality
    cellSelectorDepth : 6,
    
    // Increase depth for getTarget functionality
    rowSelectorDepth : 11,

    
    trackMouseInTimeHeader : false,
    
    // private
    translateToScheduleCoordinate : function() {
        throw 'Abstract method call';
    },
    
    // private
    translateToPageCoordinate : function(xy) {
        return [xy[0] + this.mainBody.getLeft(), xy[1] + this.mainBody.getTop()];
    },

    // private
    rowPosRe : new RegExp('sch-header-row-([^\\s]+)'),

    // private
    initTemplates: function () {
        this.templates = this.templates || {};
        var ts = this.templates;
        
        // Overridden for tclass placeholder
        if (!ts.header) {
            ts.header = new Ext.Template(
                '<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}" class="sch-header-row-{tclass}">',
                    '<thead>',
                        '<tr class="x-grid3-hd-row">{cells}</tr>',
                    '</thead>',
                '</table>'
            );
        }

        // Added gcell for multiple header rows
        if (!ts.gcell) {
            ts.gcell = new Ext.XTemplate(
				'<td class="x-grid3-hd x-grid3-gcell x-grid3-td-{id} ux-grid-hd-group-row-{row} {cls}" style="{style}">',
				'<div {tooltip} class="x-grid3-hd-inner x-grid3-hd-{id}" unselectable="on" style="{istyle}">',
				'{value}</div></td>'
			);
        }

        Sch.AbstractSchedulerView.superclass.initTemplates.call(this);
    },

    // private
    getColumnHeaderCls: function (i, last) {
        var css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
        return css + (this.cm.config[i].hCls || '') + (this.cm.config[i].headerCls || '');
    },

    
    getAccumulatedColumnWidth: function (start, end) {
        var cm = this.cm,
            w = 0,
            i;

        for (i = start; i < end; i++) {
            if (!cm.isHidden(i)) {
                w += cm.getColumnWidth(i);
            }
        }

        return w;
    },

    // private
    getNumberOfTimeColumns : function() {
        return this.cm.getColumnCount() - this.rightColumns.length - this.leftColumns.length;  
    },

    // private
    getLastTimeColumnIndex : function() {
        return this.cm.getColumnCount() - this.rightColumns.length - 1;  
    },

    
    getSingleUnitInPixels: function (unit) {
        return Sch.util.Date.getUnitToBaseUnitRatio(this.getColumnModelUnit(), unit) * this.cm.getColumnWidth(this.leftColumns.length);
    },

    
    getScheduleSectionWidth: function () {
        return this.cm.getTotalWidth() - this.getLeftSectionWidth() - this.getRightSectionWidth();
    },

    
    getLeftSectionWidth: function () {
        return this.getAccumulatedColumnWidth(0, this.leftColumns.length);
    },

    
    getRightSectionWidth: function () {
        var colCount = this.cm.getColumnCount();
        return this.getAccumulatedColumnWidth(colCount - this.rightColumns.length, colCount);
    },
    
    //private
    resolveHeaderGroupCell : function (cell) {
        var cls = cell.className;
            
        if (cls) {
            var pos;

            if (cls.match(/ux-grid-hd-group-row-top/)) {
                pos = "top";
            } else {
                pos = "middle";
            }

            return {
                position : pos,
                index : cls.match(this.colRe)[1]
            };
        }
    },
    
    //private
    onHeaderDoubleClick: function (g, colIndex, e) {
        var t = e.getTarget('.sch-timeheader');

        if (!t) return;

        var isGroupRow = !!t.className.match('ux-grid-hd-group-cell'),
            headerCfg,
            cm = g.getColumnModel();

        if (isGroupRow) {
            var rowGroup = this.resolveHeaderGroupCell(e.getTarget('.x-grid3-hd'));
            headerCfg = this.headerRows[rowGroup.position][rowGroup.index];
        } else {
            headerCfg = this.timeAxis.getAt(colIndex - this.leftColumns.length);
        }
        
        this.fireEvent('timeheaderdblclick', this, headerCfg.start, headerCfg.end, e);
    },

    
    //private
    handleHdMove: Ext.grid.GridView.prototype.handleHdMove.createInterceptor(function (e) {
        // Block the default resize mechanism for group header row cells
        return !e.getTarget('.ux-grid-hd-group-cell');
    }),


    
    getTimeSpanRegion: function (startDate, endDate) {
        var startX = this.getXFromDate(startDate),
            endX = this.getXFromDate(endDate || startDate);

        return new Ext.lib.Region(0, endX, this.mainBody.getHeight(), startX);
    },

    
    scrollEventIntoView: function (eventRec, highlight) {
        var el = this.getElementFromEventRecord(eventRec);

        if (el) {
            el.scrollIntoView(this.scroller);

            if (highlight) {
                if (typeof highlight === "boolean") {
                    el.highlight();
                } else {
                    el.highlight(null, highlight);
                }
            }
        }
    },

    // private
    onHeaderClick: Ext.grid.GridView.prototype.onHeaderClick.createInterceptor(function (g, index, e) {
        // Block the default sort mechanism when clicking on a grouping header row
        return !e.getTarget('.ux-grid-hd-group-cell') && !e.getTarget('.ux-grid-hd-nogroup-cell');
    }),
    
    // private
    calculateTimeColumnWidth : function(proposedTimeColumnWidth) {
        var w = 0,
            timelineUnit = this.getColumnModelUnit(),
            nbrTimeColumns = this.timeAxis.getCount(),
            ratio = Number.MAX_VALUE;
        
        if (this.grid.snapToIncrement) {
            var res = this.timeAxis.getResolution(),
                unit = res.unit,
                resIncr = res.increment;
            
            ratio = Sch.util.Date.getUnitToBaseUnitRatio(timelineUnit, unit) * resIncr;
        }

        var measuringUnit = Sch.util.Date.getMeasuringUnit(timelineUnit);
        
        ratio = Math.min(ratio, Sch.util.Date.getUnitToBaseUnitRatio(timelineUnit, measuringUnit));

        w = this.forceFit ? Math.floor(this.getAvailableWidthForSchedule() / nbrTimeColumns) : proposedTimeColumnWidth;
            
        if (!this.forceFit || ratio < 1) {
            w = Math.floor(ratio * w) / ratio;
        }
        return w;
    },

    // private
    // This function fits respects the original width of the static left/right columns and
    // fits the time columns into the space that's left
    fitColumns: function (preventRefresh) {
        var cm = this.cm,
            nbrToFit = this.getNumberOfTimeColumns(),
            proposedWidth = Math.floor(this.getAvailableWidthForSchedule() / nbrToFit),
            w = this.calculateTimeColumnWidth(proposedWidth);
            
        for (var i = 0; i < nbrToFit; i++) {
            cm.setColumnWidth(i + this.leftColumns.length, w, true);
        }

        if (preventRefresh !== true) {
            this.updateAllColumnWidths();
        }
    },

    
    getElementFromEventRecord : function(record) {
        return Ext.get(this.grid.eventPrefix + record.id);
    },
    
    // private
    isTimeHeaderCell: function (t) {
        return !!Ext.fly(t).up('td.sch-timeheader');
    },

    // private
    getLeftGroupWidth: function (group) {
        var width = this.getAccumulatedColumnWidth(0, this.leftColumns.length);
        return (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2)) ? width : Math.max(width - this.borderWidth, 0);
    },

    // private
    getRightGroupWidth: function (group) {
        var width,
            colCount = this.cm.getColumnCount();

        if (this.rightColumns.length) {
            width = this.getAccumulatedColumnWidth(colCount - this.rightColumns.length, colCount);
            return (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2)) ? width : Math.max(width - this.borderWidth, 0);
        } else {
            return 0;
        }
    },

    
    // private, overwritten to be able to highlight header cell even though menu is disabled
    handleHdOver: function (e, t) {
        var hd = this.findHeaderCell(t);
        if (hd && !this.headersDisabled) {
            this.activeHdRef = t;
            this.activeHdIndex = this.getCellIndex(hd);
            var fly = this.fly(hd);
            this.activeHdRegion = fly.getRegion();

            if (this.isTimeHeaderCell(t)) {
                // Time column header cell
                if (this.trackMouseInTimeHeader) {
                    fly.addClass('x-grid3-hd-over');

                    if (!this.cm.isMenuDisabled(this.activeHdIndex)) {
                        this.activeHdBtn = fly.child('.x-grid3-hd-btn');
                        if (this.activeHdBtn) {
                            this.activeHdBtn.dom.style.height = (hd.firstChild.offsetHeight - 1) + 'px';
                        }
                    }
                }
            } else {
                if (!this.cm.isMenuDisabled(this.activeHdIndex)) {
                    fly.addClass('x-grid3-hd-over');
                    this.activeHdBtn = fly.child('.x-grid3-hd-btn');
                    if (this.activeHdBtn) {
                        this.activeHdBtn.dom.style.height = (hd.firstChild.offsetHeight - 1) + 'px';
                    }
                }
            }
        }
    },

    // private
    resolveColumnIndex: function (x) {
        var cm = this.cm,
            index = -1,
            totalWidth = this.getLeftSectionWidth(),
            colWidth;

        if (x >= totalWidth) {
            for (index = this.leftColumns.length, lastIndex = cm.getColumnCount() - 1; index < lastIndex; index++) {
                if (!cm.isHidden(index)) {
                    colWidth = cm.getColumnWidth(index);
                    if (x >= totalWidth && x <= (totalWidth + colWidth)) {
                        break;
                    }
                    totalWidth += colWidth;
                }
            }
        }
        
        return index;
    },

    
    // private
    onColumnWidthUpdated: function () {
        Sch.AbstractSchedulerView.superclass.onColumnWidthUpdated.apply(this, arguments);
        this.updateGroupStyles();
    },

    // private
    onAllColumnWidthsUpdated: function () {
        Sch.AbstractSchedulerView.superclass.onAllColumnWidthsUpdated.apply(this, arguments);
        this.updateGroupStyles();
    },

    // private
    onColumnHiddenUpdated: function () {
        Sch.AbstractSchedulerView.superclass.onColumnHiddenUpdated.apply(this, arguments);
        this.updateGroupStyles();
    },

    // private
    getHeaderCell: function (index) {
        return this.mainHd.query(this.cellSelector)[index];
    },

    // private
    findHeaderCell: function (el) {
        return el ? this.fly(el).findParent('td.x-grid3-hd', this.cellSelectorDepth) : false;
    },

    // private
    findHeaderIndex: function (el) {
        var cell = this.findHeaderCell(el);
        return cell ? this.getCellIndex(cell) : false;
    },

    
    getXYFromDate: function (date) {
        return [this.getXFromDate(date), 0];
    },

    
    getXFromDate: function (date) {
        var x = -1,
            tick = this.timeAxis.getTickFromDate(date);
        
        if (tick >= 0) {
            var colIndex = this.leftColumns.length + Math.floor(tick),
                colWidth = this.getAccumulatedColumnWidth(0, colIndex);

            x = colWidth + (colIndex < this.cm.getColumnCount() ? this.cm.getColumnWidth(colIndex) * (tick - Math.floor(tick)) : 0);
        }

        return x;
    },

    
    getDateFromXY : function(xy) {
        return this.getDateFromX(xy[0]);
    },

    
    getDateFromX: function (x) {
        x = this.translateToScheduleCoordinate(x);

        var colIndex = this.resolveColumnIndex(x),
            tick = colIndex - this.leftColumns.length,
            maxCol = (this.getNumberOfTimeColumns() + this.leftColumns.length);
        
        if (tick < 0 || colIndex > maxCol) {
            return null;
        } else {
            var diff = x - this.getAccumulatedColumnWidth(0, colIndex);
            if (colIndex < maxCol) {
                tick += Math.min(1, diff / this.cm.getColumnWidth(this.leftColumns.length));
            } else if (diff > 2) {
                return null;
            }
            return this.timeAxis.getDateFromTick(tick);
        }
    },

    // private
    getHeaderRowCells : function(row) {
        var leftGroupWidth = this.getLeftGroupWidth(),
            rightGroupWidth = this.getRightGroupWidth(),
            r = this.headerRows[row], cells = [], cls, group,
            ts = this.templates,
            headerConfig = this.grid.getHeaderConfig();
    
        for (i = 0, len = r.length; i < len; i++) {
            group = r[i];

            if (i === 0 && leftGroupWidth) {
                width = leftGroupWidth;
                cls = '';
            } else if (rightGroupWidth && i === len - 1) {
                width = rightGroupWidth;
                cls = '';
            } else {
                width = this.getHeaderGroupCellWidth(group.start, group.end, headerConfig[row].unit, headerConfig[row].increment || 1);
                cls = 'sch-timeheader ';

                if (width <= 0) {
                    cls += ' x-hidden ';
                }
            }

            cells[i] = ts.gcell.apply({
                cls: cls + (group.headerCls || '') + (group.header ? ' ux-grid-hd-group-cell' : ' ux-grid-hd-nogroup-cell'),
                id: i,
                row: row,
                style: 'width:' + width + 'px;' + (group.align ? 'text-align:' + group.align + ';' : ''),
                istyle: group.align == 'right' ? 'padding-right:16px' : '',
                value: group.header || '&#160;'
            });
        }
        return cells;
    },
    
    // private
    getHeaderGroupCellWidth : function(start, end, headerRowUnit, headerRowIncrement) {
        var baseUnit = this.getColumnModelUnit(),
            baseIncrement = this.getColumnModelIncrement(),
            width,
            measuringUnit = Sch.util.Date.getMeasuringUnit(baseUnit),
            durationInMeasuringUnit = Sch.util.Date.getDurationInUnit(start, end, measuringUnit);
        
        if (this.timeAxis.isContinuous()) {
            width = durationInMeasuringUnit * this.getSingleUnitInPixels(measuringUnit) / baseIncrement;
        } else {
            width = this.getXFromDate(end) - this.getXFromDate(start);
        }
        
        if (!(Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2))) {
            width -= 2;
        }
        
        return width;
    },

    // private
    getColumnModelUnit : function() {
        var headerConfig = this.grid.getHeaderConfig();
        return headerConfig.bottom ? headerConfig.bottom.unit : headerConfig.middle.unit;
    },

    // private
    getColumnModelIncrement : function() {
        var headerConfig = this.grid.getHeaderConfig();
        return (headerConfig.bottom ? headerConfig.bottom.increment : headerConfig.middle.increment) || 1;
    },

    // private
    updateColumnModel : function (initial) {
        if (!this.isValidHeaderConfig()) {
            throw 'Invalid header configuration, please check your header definition';
        }

        // Give time data to ColumnFactory to get column objects for the time columns
        var g = this.grid,
            proposedTimeColumnWidth = g.getTimeColumnWidth(),
            headerConfig = g.getHeaderConfig(),
            ta = this.timeAxis,
            start = ta.getStart(), 
            end = ta.getEnd(), 
            colDefaults = { renderer: this.timeColumnRenderer, scope: this, width : this.calculateTimeColumnWidth(proposedTimeColumnWidth)},
            columnConfig = Sch.ColumnFactory.createColumns(this.timeAxis, headerConfig, colDefaults),
            headerRows = columnConfig.headerRows;
        
        if (headerRows) {
            for (var r in headerRows) {
                if (headerRows.hasOwnProperty(r)) {
                    if (this.leftColumns.length > 0) {
                        headerRows[r].unshift({ colspan: this.leftColumns.length });
                    }

                    if (this.rightColumns.length > 0) {
                        headerRows[r].push({ colspan: this.rightColumns.length });
                    }
                }
            }
        }

        Ext.apply(this, {
            headerRows: headerRows
        });

        // Remove old id property so that the column model can be configured properly (ordering/lookup etc)
        Ext.each(this.rightColumns, function(c) {
            delete c.id;
        });

        // Append the columns to the existing column array and set a new config
        this.cm.setConfig(this.leftColumns.concat(columnConfig.columns).concat(this.rightColumns), initial);
    },

    
    getSnapPixelAmount : function() {
        if (this.grid.snapToIncrement) {
            var resolution = this.timeAxis.getResolution();
            
            return (resolution.increment || 1) * this.getSingleUnitInPixels(resolution.unit);
        } else {
            return 1;
        }
    },

    // private
    isValidHeaderConfig: function() {
        var headerConfig = this.grid.getHeaderConfig(),
            cmpResult,
            valid = true;
        
        if (headerConfig.bottom) {
            cmpResult = Sch.util.Date.compareUnits(headerConfig.bottom.unit, headerConfig.middle.unit);
            if (cmpResult > 0 || (cmpResult === 0 && (headerConfig.bottom.increment || 1) > (headerConfig.middle.increment || 1))) {
                valid = false;
            }
        }

        if (headerConfig.top) {
            cmpResult = Sch.util.Date.compareUnits(headerConfig.middle.unit, headerConfig.top.unit);
            if (cmpResult > 0 || (cmpResult === 0 && (headerConfig.middle.increment || 1) > (headerConfig.top.increment || 1))) {
                valid = false;
            }
        }

        return valid;
    },

    // private
    initializeColumns: function () {
        var cm = this.cm,
            cfg = cm.config,
            nbrColumns = cfg.length,
            dividerIndex = -1;

        for (var i = 0; i < nbrColumns; i++) {
            if (cfg[i].position && cfg[i].position === 'right') {
                dividerIndex = i;
                break;
            }
        }

        if (dividerIndex >= 0) {
            this.rightColumns = cfg.slice(dividerIndex, nbrColumns);
        } else {
            this.rightColumns = [];
        }

        this.leftColumns = cfg.slice(0, dividerIndex >= 0 ? dividerIndex : nbrColumns);
    },
    
    // private
    onSchedulerRender: function () {
        // Modify the built-in column drag drop object
        if (this.columnDrop) {
            Ext.apply(this.columnDrop, {
                getTargetFromEvent: this.columnDropGetTargetFromEvent.createDelegate(this)
            });
        }

        // Prevent moving time columns
        if (this.columnDrag) {
            this.columnDrag.onBeforeDrag = function (data, e) {
                return !e.getTarget('.sch-timeheader');
            };
        }
    },
    
    // private
    columnDropGetTargetFromEvent: function (e) {
        var t = Ext.lib.Event.getTarget(e),
            cindex = this.findCellIndex(t);

        // Prevent user from dropping static columns among the time columns
        if (cindex !== false && cindex < this.leftColumns.length) {
            return this.getHeaderCell(cindex);
        }
        return false;
    },

    // private
    init: function (grid) {
        Ext.apply(this, {
            grid: grid,
            rowHeight : grid.rowHeight,
            eventBorderWidth : grid.eventBorderWidth,
            cm: grid.getColumnModel(),
            timeAxis: grid.timeAxis
        });

        this.on('headerdblclick', this.onHeaderDoubleClick, this);
        this.timeAxis.on('reconfigure', function() { this.updateColumnModel(); }, this);
        
        this.initializeColumns(true);

        this.updateColumnModel();

        grid.on({
            afterrender: this.onSchedulerRender,
            headerdblclick: this.onHeaderDoubleClick,
            scope: this
        });
        Sch.AbstractSchedulerView.superclass.init.apply(this, arguments);
    },

    // private
    timeColumnRenderer : function() {
        throw 'Abstract method call';
    }
});



Ext.ns('Sch');


Sch.HorizontalSchedulerView = Ext.extend(Sch.AbstractSchedulerView, {
    forceFit: true,
    
    
    setBarMargin : function(margin, preventRefresh) {
        this.barMargin = margin;
        if (!preventRefresh) {
            this.refresh();
        }
    },
    
    setRowHeight : function(height) {
        this.rowHeight = height;
        
        if (this.grid.viewReady) {
            this.refresh();
        }
    },

    
    managedEventLayout : true,
    
    
    barMargin : 1,

    // private
    constructor: function (config) {
        Sch.HorizontalSchedulerView.superclass.constructor.call(this, config);
        
        if (this.forceFit) {
            this.refresh = this.refresh.createInterceptor(function () { this.fitColumns(true); });
        }
    },

    // private
    onEventAdd: function (s, recs) {
        var resource;
        for (var i = 0, l = recs.length; i < l; i++) {
            resource = this.grid.getResourceByEventRecord(recs[i]);
            if (resource) {
                this.refreshRow(resource);
            }
        }
    },

    // private
    onEventRemove: function (s, rec) {
        var el = this.getElementFromEventRecord(rec);

        if (el) {
            el.fadeOut({
                remove: true,
                callback: function () {
                    var resource = this.grid.getResourceByEventRecord(rec);
                    if (resource) {
                        this.refreshRow(resource);
                    }
                },
                scope: this
            });
        }
    },

    // private
    onEventUpdate: function (s, rec, operation, hashPrevious) {
        var resource;

        if (hashPrevious && hashPrevious.ResourceId) {
            // If an event has been moved to a new row, refresh old row first
            resource = this.ds.getAt(this.ds.findExact('Id', hashPrevious.ResourceId));
            this.refreshRow(resource);
        }

        resource = this.grid.getResourceByEventRecord(rec);
        if (resource) {
            this.refreshRow(resource);
        }
    },

    // private
    init: function (grid) {
        Ext.apply(this, {
            timeCellRenderer : grid.timeCellRenderer,
            eventBodyTemplate : grid.eventBodyTemplate,
            eventTemplate : grid.eventTemplate,
            eventRenderer : grid.eventRenderer,
            eventBarTextField : grid.eventBarTextField
        });

        grid.on({
            columnresize: this.refreshView,
            resize: this.refreshView,
            scope: this
        });
        Sch.HorizontalSchedulerView.superclass.init.apply(this, arguments);
    },

    
    refreshView: function (g) {
        if (this.grid.viewReady) {
            this.refresh(true);
        }
    },

    // private
    updateSortIcon: function (col, dir) {
        var sc = this.sortClasses;
        var hds = this.mainHd.select(this.cellSelector).removeClass(sc);
        hds.item(col).addClass(sc[dir == "DESC" ? 1 : 0]);
    },

    // private
    getAvailableWidthForSchedule : function() {
        return this.grid.getGridEl().getWidth(true) - this.getScrollOffset() - this.getLeftSectionWidth() - this.getRightSectionWidth();
    },

    // private
    updateGroupStyles: function () {
        var headerRows = this.headerRows;

        if (!headerRows) {
            return;
        }

        var tables = this.mainHd.query('.x-grid3-header-offset > table'),
            headerConfig = this.grid.getHeaderConfig(),
		    leftGroupWidth = this.getLeftGroupWidth(),
		    rightGroupWidth = this.getRightGroupWidth(),
		    tw = this.cm.getTotalWidth();

        for (var row = 0; row < tables.length; row++) {
            tables[row].style.width = tw + 'px';

            if (row < tables.length - 1) {
                var cells = tables[row].firstChild.firstChild.childNodes;

                for (var i = 0; i < cells.length; i++) {
                    
                    if (i === 0 && leftGroupWidth > 0) {
                        cells[i].style.width = leftGroupWidth + 'px';
                    } else if (i === cells.length - 1 && rightGroupWidth > 0) {
                        cells[i].style.width = rightGroupWidth + 'px';
                    } else {
                        var rowPos = headerConfig.top ? (row === 0 ? 'top' : 'middle') : 'middle',
                            group = headerRows[rowPos][i],
                            width = this.getHeaderGroupCellWidth(group.start, group.end, headerConfig[rowPos].unit, headerConfig[rowPos].increment || 1);

                        cells[i].style.width = width + 'px';
                    }
                }
            }
        }
    },

    // Grouping header related functionality, based on Condors GroupHeaderGrid plugin
    // private
    renderHeaders: function () {
    
        var ts = this.templates,
		         headers = [],
		         cm = this.cm,
		         headerRows = this.headerRows,
		         width,
		         id,
		         group,
		         tw = this.getTotalWidth(),
                 headerConfig = this.grid.getHeaderConfig(),
		         i,
		         len;
        
        if (headerRows) {
            if (headerRows.top) {
                var topRowCells = this.getHeaderRowCells('top');
                headers.push(ts.header.apply({
                        tstyle: 'width:' + tw,
                        tclass :'top',
                        cells: topRowCells.join('')
                    })
                );
            }

            if (headerRows.middle) {
                var middleRowCells = this.getHeaderRowCells('middle');
                headers.push(ts.header.apply({
                        tclass :'middle',
                        tstyle: 'width:' + tw,
                        cells: middleRowCells.join('')
                    })
                );
            }
        }

        // The default column header rendering, overriden to be able to assign a css class to each header cell
        len = cm.getColumnCount();

        var ct = ts.hcell,
            cb = [],
            p = {},
            last = len - 1;

        for (i = 0; i < len; i++) {
            p.id = cm.getColumnId(i);
            p.value = cm.getColumnHeader(i) || '';
            p.style = this.getColumnStyle(i, true);
            p.tooltip = this.getColumnTooltip(i);
            p.css = this.getColumnHeaderCls(i, last);
            if (cm.config[i].align == 'right') {
                p.istyle = 'padding-right:16px';
            } else {
                delete p.istyle;
            }

            cb[cb.length] = ct.apply(p);
        }
        headers.push(ts.header.apply({ cells: cb.join(''), tstyle: 'width:' + tw + ';', tclass : headerRows.middle ? 'bottom' : 'middle' }));
        return headers.join('');
    },

    
    translateToScheduleCoordinate: function (x) {
        return x - this.el.getX();
    },

    // private
    timeColumnRenderer : function (v, m, rec, row, col, ds, events) {

        var cellResult = '&#160;',
            scheduler = this.grid,
            cm = this.cm,
            ta = this.timeAxis,
            viewStart = ta.getStart(),
            viewEnd = ta.getEnd(),
            firstScheduleColumnIndex = this.leftColumns.length,
            tickIndex = col - firstScheduleColumnIndex,
            prevEnd = (col === firstScheduleColumnIndex) ? null : ta.getAt(tickIndex - 1).end,
            colWidth = cm.getColumnWidth(col),
            colTick = ta.getAt(tickIndex),
            colStart = colTick.start,
            colEnd = colTick.end;
            
        // Call timeCellRenderer to be able to set css/style properties on scheduler time cells
        this.timeCellRenderer.call(scheduler, events, m, rec, row, col, ds, colStart, colEnd);
        
        // Iterate events (belonging to current row) passed by the schedulerview
        for (var i = 0, l = events.getCount(); i < l; i++) {
            var event = events.items[i],
                start = event.data.StartDate,
                end = event.data.EndDate;
                
            if (start && end) {
                // Determine if the event should be rendered or not
                var doRender;
               
                if (col === firstScheduleColumnIndex) {
                    doRender = Sch.util.Date.betweenLesser(start, colStart, colEnd) || (start < colStart && end > colStart); 
                } else {
                    doRender = Sch.util.Date.betweenLesser(start, prevEnd, colEnd) && ((end > colStart) || end-start === 0);
                }
                
                if (doRender) {
                    var availableTimeInColumn = colEnd - colStart,
                        leftOffset = ((Sch.util.Date.max(start, colStart) - colStart) / availableTimeInColumn) * colWidth,
                        itemWidth = this.getXFromDate(Sch.util.Date.min(end, viewEnd)) - this.getXFromDate(Sch.util.Date.max(start, viewStart)),
                        tplData = {
                            id: event.id,
                            internalcls : (event.dirty ? 'sch-dirty' : '') + (end > viewEnd ? ' sch-event-endsoutside ' : '') + (start < viewStart ? ' sch-event-startsoutside' : ''),
                            left : leftOffset, 
                            width : Math.max(1, Ext.isBorderBox ? itemWidth : itemWidth - this.eventBorderWidth)
                        };
                    
                    if (this.managedEventLayout) {
                        tplData.top = this.barMargin;
                        tplData.height = this.rowHeight - (2 * this.barMargin) - (Ext.isBorderBox ? 0 : this.eventBorderWidth);
                    }

                    if (this.eventRenderer) {
                        // User has specified a renderer fn, either to return a simple string, or an object intended for the eventBodyTemplate
                        var value = this.eventRenderer.call(scheduler, event, rec, tplData, row, col);
                        if (Ext.isObject(value) && this.eventBodyTemplate) {
                            tplData.body = this.eventBodyTemplate.apply(value);
                        } else {
                            tplData.body = value;
                        }
                    } else if (this.eventBarTextField) {
                        // User has specified a field in the data model to read from
                        tplData.body = event.data[this.eventBarTextField];
                    } else if (this.eventBodyTemplate){
                        // User has specified an eventBodyTemplate, but no renderer - just apply the entire event record data.
                        tplData.body = this.eventBodyTemplate.apply(event.data);
                    }

                    cellResult += this.eventTemplate.apply(tplData);
                }
            }
        }

        m.css += ' sch-timetd';

        // Z-index is trouble in IE, thanks Condor for this fix
        if (Ext.isIE) {
            m.attr += ' style="z-index:' + (cm.getColumnCount() - col) + '"';
        }
        return cellResult;
    },

    // private
    doRender: function (cs, rs, ds, startRow, colCount, stripe) {
        var g = this.grid, ts = this.templates, ct = ts.cell, rt = ts.row, last = colCount - 1;
        var tstyle = 'width:' + this.getTotalWidth() + ';height:' + this.grid.getRowHeight() + 'px';
        // buffers
        var buf = [], cb, c, p = {}, rp = { tstyle: tstyle }, r, events, totalScheduleWidth = this.getScheduleSectionWidth();

        for (var j = 0, len = rs.length; j < len; j++) {
            r = rs[j]; cb = [];
            var rowIndex = (j + startRow),
                resId = r.get('Id');

            // @Modification: Events for current row
            events = this.es.data.filterBy(function (brec) {
                return brec.data.ResourceId == resId;
            });

            for (var i = 0; i < colCount; i++) {
                c = cs[i];
                p.id = c.id;
                p.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '');
                p.attr = p.cellAttr = "";

                // @Modification: Added events to param list
                p.value = c.renderer.call(c.scope || c, r.data[c.name], p, r, rowIndex, i, ds, events);
                p.style = c.style;

                cb[cb.length] = ct.apply(p);
            }
            var alt = [];
            if (stripe && ((rowIndex + 1) % 2 === 0)) {
                alt[0] = "x-grid3-row-alt";
            }
            if (r.dirty) {
                alt[1] = " x-grid3-dirty-row";
            }
            rp.cols = colCount;
            if (this.getRowClass) {
                alt[2] = this.getRowClass(r, rowIndex, rp, ds);
            }
            rp.alt = alt.join(" ");
            rp.cells = cb.join("");
            buf[buf.length] = rt.apply(rp);
        }
        return buf.join("");
    },

    // Overridden to attach listeners to eventstore
    initData: function (ds, cm) {
        Sch.HorizontalSchedulerView.superclass.initData.apply(this, arguments);
        
        if (!this.es) {
            this.es = this.grid.eventStore;
            this.es.on({
                scope: this,
                datachanged: this.onDataChange,
                add: this.onEventAdd,
                update: this.onEventUpdate,
                remove: this.onEventRemove,
                clear: this.onClear
            });
        }

        if (this.cm) {
            this.cm.un('hiddenchange', this.refreshView, this);
        }
        if (cm) {
            cm.on('hiddenchange', this.refreshView, this);
        }
    },

    initTemplates: function () {
        this.templates = this.templates || {};
        
        Ext.applyIf(this.templates, {
            eventBodyTemplate : this.eventBodyTemplate,
            eventTemplate : this.eventTemplate
        });
        Sch.HorizontalSchedulerView.superclass.initTemplates.apply(this, arguments);
    },

  
    // private
    resolveResource : function(t) {
        var retval = null,
            index = this.findRowIndex(t);
        if (index >= 0) {
            retval = this.ds.getAt(index);
        }
        return retval;
    },

    
    getScheduleRegion: function (resourceRecord, eventRecord) {
        var region = resourceRecord ? Ext.fly(this.getCell(this.ds.indexOf(resourceRecord), this.leftColumns.length)).getRegion() : this.mainBody.getRegion(),
            taStart = this.timeAxis.getStart(),
            taEnd = this.timeAxis.getEnd(),
            dateConstraints = this.grid.getDateConstraints(resourceRecord, eventRecord) || { start : taStart, end : taEnd },
            cm = this.cm,
            left = this.translateToPageCoordinate([this.getXFromDate(Sch.util.Date.max(taStart, dateConstraints.start)), 0])[0],
            right = this.translateToPageCoordinate([this.getXFromDate(Sch.util.Date.min(taEnd, dateConstraints.end)), 0])[0],
            top = region.top + this.barMargin,
            bottom = (resourceRecord ? (region.top + this.rowHeight) : region.bottom) - this.barMargin;
            
        return new Ext.lib.Region(top, right, bottom, left);
    }
});

// Adaptations for GridView changes in Ext 3.3
if (Ext.getMajorVersion() >= 3 && Ext.getMinorVersion() >= 3) {
    Ext.override(Sch.HorizontalSchedulerView, {
        refreshRow: function (record) {
            var store = this.ds,
                colCount = this.cm.getColumnCount(),
                columns = this.getColumnData(),
                last = colCount - 1,
                cls = ['x-grid3-row'],
                rowParams = {
                    tstyle: String.format("width: {0};height:{1}px", this.getTotalWidth(), this.grid.getRowHeight())
                },
                colBuffer = [],
                cellTpl = this.templates.cell,
                rowIndex, row, column, meta, css, i;

            if (Ext.isNumber(record)) {
                rowIndex = record;
                record = store.getAt(rowIndex);
            } else {
                rowIndex = store.indexOf(record);
            }


            if (!record || rowIndex < 0) {
                return;
            }

            // @Modification: Events for current row
            var resId = record.get('Id'),
                events = this.es.data.filterBy(function (brec) {
                    return brec.data.ResourceId == resId;
                });

            for (i = 0; i < colCount; i++) {
                column = columns[i];

                if (i === 0) {
                    css = 'x-grid3-cell-first';
                } else {
                    css = (i == last) ? 'x-grid3-cell-last ' : '';
                }

                meta = {
                    id: column.id,
                    style: column.style,
                    css: css,
                    attr: "",
                    cellAttr: ""
                };

                meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store, events);

                if (Ext.isEmpty(meta.value)) {
                    meta.value = '&#160;';
                }

                if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') {
                    meta.css += ' x-grid3-dirty-cell';
                }

                colBuffer[i] = cellTpl.apply(meta);
            }

            row = this.getRow(rowIndex);
            row.className = '';

            if (this.grid.stripeRows && ((rowIndex + 1) % 2 === 0)) {
                cls.push('x-grid3-row-alt');
            }

            if (this.getRowClass) {
                rowParams.cols = colCount;
                cls.push(this.getRowClass(record, rowIndex, rowParams, store));
            }

            this.fly(row).addClass(cls).setStyle(rowParams.tstyle);
            rowParams.cells = colBuffer.join("");
            row.innerHTML = this.templates.rowInner.apply(rowParams);

            this.fireEvent('rowupdated', this, rowIndex, record);
        }
    });
}



Ext.ns('Sch');

(function() {
    if (!Ext.ux || !Ext.ux.grid || !Ext.ux.grid.LockingGridView) {
        return;
    }

    var lgvp = Ext.ux.grid.LockingGridView.prototype;
   
    var abstractPrototype = {

        lockText : lgvp.lockText,
	    unlockText : lgvp.unlockText,
	    rowBorderWidth : lgvp.rowBorderWidth,
	    lockedBorderWidth : lgvp.lockedBorderWidth,
	    getEditorParent : lgvp.getEditorParent,
	    initElements : lgvp.initElements,
	    getLockedRows : lgvp.getLockedRows,
	    getLockedRow : lgvp.getLockedRow,
	    getCell : lgvp.getCell,
	    addRowClass : lgvp.addRowClass,
	    removeRowClass : lgvp.removeRowClass,
	    removeRow : lgvp.removeRow,
	    removeRows : lgvp.removeRows,
	    syncScroll : lgvp.syncScroll,
	    updateAllColumnWidths : lgvp.updateAllColumnWidths,
	    updateColumnWidth : lgvp.updateColumnWidth,
	    updateColumnHidden : lgvp.updateColumnHidden,
	    processRows : lgvp.processRows,
	    afterRenderUI: lgvp.afterRenderUI,
	    renderUI : lgvp.renderUI,
	    getOffsetWidth : lgvp.getOffsetWidth,
	    getResolvedXY : lgvp.getResolvedXY,
	    syncFocusEl : lgvp.syncFocusEl,
	    ensureVisible : lgvp.ensureVisible,
	    insertRows : lgvp.insertRows,
        getColumnStyle : lgvp.getColumnStyle,
	    getLockedWidth : lgvp.getLockedWidth,
	    getTotalWidth : lgvp.getTotalWidth,
	    renderBody : lgvp.renderBody,
	    refreshRow : lgvp.refreshRow,
	    refresh : lgvp.refresh,
	    onDenyColumnLock : lgvp.onDenyColumnLock,
	    onColumnLock : lgvp.onColumnLock,
	    handleHdMenuClick : lgvp.handleHdMenuClick,
	    handleHdDown : lgvp.handleHdDown,
	    syncHeaderHeight : lgvp.syncHeaderHeight,
	    updateLockedWidth : lgvp.updateLockedWidth,
        syncRowHeights : lgvp.syncRowHeights,       // New in Ext 3.3
	    layout : lgvp.layout,
	    masterTpl :new Ext.Template(
			'<div class="x-grid3" hidefocus="true">',
				'<div class="x-grid3-locked">',
					'<div class="x-grid3-header"><div class="x-grid3-header-inner"><div class="x-grid3-header-offset" style="{lstyle}">{lockedHeader}</div></div><div class="x-clear"></div></div>',
					'<div class="x-grid3-scroller"><div class="x-grid3-body" style="{lstyle}">{lockedBody}</div><div class="x-grid3-scroll-spacer"></div></div>',
				'</div>',
				'<div class="x-grid3-viewport x-grid3-unlocked">',
					'<div class="x-grid3-header"><div class="x-grid3-header-inner"><div class="x-grid3-header-offset" style="{ostyle}">{header}</div></div><div class="x-clear"></div></div>',
					'<div class="x-grid3-scroller"><div class="x-grid3-body" style="{bstyle}">{body}</div><a href="#" class="x-grid3-focus" tabIndex="-1"></a></div>',
				'</div>',
				'<div class="x-grid3-resize-marker">&#160;</div>',
				'<div class="x-grid3-resize-proxy">&#160;</div>',
			'</div>'
		),
        // Scheduler specific properties and methods
        forceFit : false,

        autoExpandTimeColumns : true,

        getAvailableWidthForSchedule : function() {
            return this.scroller.getWidth(true) - this.getScrollOffset() - this.getRightSectionWidth();
        },

        afterRender : function() {
            if (!this.forceFit && this.autoExpandTimeColumns && 
                this.getScheduleSectionWidth() < this.getAvailableWidthForSchedule()) {
                this.fitColumns();
            }  
            lgvp.afterRender.apply(this, arguments);
        },

        updateTimeColumnWidths: function (suppressRefresh) {
            if (this.forceFit) {
                return;
            }

            var cm = this.cm,
                width = this.grid.getTimeColumnWidth();

            for (var i = this.leftColumns.length, l = this.getLastTimeColumnIndex(); i <= l; i++) {
                cm.setColumnWidth(i, width, true);
            }
        
            if (!suppressRefresh) {
                this.refresh(true);
            }
        },

        // Overridden to handle multiple row headers
	    getHeaderCell : function(index){
		    var llen = this.cm.getLockedCount();
		    if(index < llen){
			    return this.lockedHd.child('table:last').dom.getElementsByTagName('td')[index];
		    }
		    return Sch.AbstractSchedulerView.prototype.getHeaderCell.call(this, index - llen);
	    },

        
        scrollToTime : function(date, animate) {
            var x = this.getXFromDate(date);
            if (x >= 0) {
                this.scroller.scrollTo('left', x, animate);
            }
        },

        updateSortIcon : function(col, dir){
            var sortClasses = this.sortClasses,
                lockedHeaders = this.lockedHd.select('table:last-child td').removeClass(sortClasses),
                headers = this.mainHd.select('table:last-child td').removeClass(sortClasses),
                lockedLen = this.cm.getLockedCount(),
                cls = sortClasses[dir == 'DESC' ? 1 : 0];
            
            if(col < lockedLen){
                lockedHeaders.item(col).addClass(cls);
            }else{
                headers.item(col - lockedLen).addClass(cls);
            }
        },

        // Returns X coordinate relative to the element containing the time columns
        getXFromDate: function (date) {
            var x = -1,
                tick = this.timeAxis.getTickFromDate(date);
        
            if (tick >= 0) {
                var wholeTicks = Math.floor(tick),
                    colWidth = this.getAccumulatedColumnWidth(this.leftColumns.length, this.leftColumns.length + wholeTicks);

                x = colWidth + (wholeTicks < this.timeAxis.getCount() ? this.cm.getColumnWidth(this.leftColumns.length + wholeTicks) * (tick - wholeTicks) : 0);
            }

            return x;
        },

        // Overridden to handle multiple multiple row headers
	    renderHeaders : function() {
	        var cm = this.cm,
			    ts = this.templates,
			    ct = ts.hcell,
			    cb = [], lcb = [],
			    p = {},
			    len = cm.getColumnCount(),
			    last = len - 1,
			    twValue = this.cm.getTotalWidth() - this.cm.getTotalLockedWidth(),
                totalScheduleWidth = this.getScheduleSectionWidth(),
			    tw = twValue + 'px',
			    lw = this.getLockedWidth(),
			    lockedHeaders = '',
			    unlockedHeaders = '',
			    i;
    	    
	        // Process standard headers		
		    for(i = 0; i < len; i++){
			    p.id = cm.getColumnId(i);
			    p.value = cm.getColumnHeader(i) || '';
			    p.style = this.getColumnStyle(i, true);
			    p.tooltip = this.getColumnTooltip(i);
                p.css = this.getColumnHeaderCls(i, last);
			    if(cm.config[i].align == 'right'){
				    p.istyle = 'padding-right:16px';
			    } else {
				    delete p.istyle;
			    }
			    if(cm.isLocked(i)){
				    lcb[lcb.length] = ct.apply(p);
			    }else{
				    cb[cb.length] = ct.apply(p);
			    }
		    }
    		
	        // Process extra row headers		
		    var headerRows = this.headerRows;
            
            if (headerRows) {
                var lockedCount = this.cm.getLockedCount();

                if (headerRows.top) {
                    var topRowCells = this.getHeaderRowCells('top'),
                        staticCell = ts.gcell.apply({
					        cls: 'x-grid3-cell-first x-grid3-cell-last ux-grid-hd-nogroup-cell',
					        value: '&#160;',
					        row: 'top',
			                id : 0,
				            style: 'width:' + lw
			            }),
                        value = ts.header.apply({
			                cells : staticCell,
                            tclass : 'top',
			                tstyle:'width:'+lw+';'
			            });
                    
                    if (lockedCount > 0) {
                        lockedHeaders += value;
                    } else {
                        topRowCells.splice(1, 0, staticCell);
                    }

                    unlockedHeaders += ts.header.apply({
				        tstyle: 'width:' + tw,
                        tclass : 'top',
				        cells: topRowCells.slice(1).join('')
			        });
                }

                if (headerRows.middle) {
                    var middleRowCells = this.getHeaderRowCells('middle'),
                        staticCell = ts.gcell.apply({
					        cls: 'x-grid3-cell-first x-grid3-cell-last ux-grid-hd-nogroup-cell',
					        value: '&#160;',
					        row: 'middle',
			                id : 0,
				            style: 'width:' + lw
			            }),
                        value = ts.header.apply({
			                cells : staticCell,
                            tclass : 'middle',
			                tstyle:'width:'+lw+';'
			            });
                    
                    if (lockedCount > 0) {
                        lockedHeaders += value;
                    } else {
                        middleRowCells.splice(1, 0, staticCell);
                    }

                    unlockedHeaders += ts.header.apply({
				        tstyle: 'width:' + tw,
                        tclass : 'middle',
				        cells: middleRowCells.slice(1).join('')
			        });
                }
		    }
    		
		    unlockedHeaders += ts.header.apply({cells: cb.join(''), tstyle:'width:'+tw+';', tclass : headerRows.middle ? 'bottom' : 'middle' });
		    lockedHeaders += ts.header.apply({cells: lcb.join(''), tstyle:'width:'+this.getLockedWidth()+';', tclass : headerRows.middle ? 'bottom' : 'middle' });
    		
		    return [
		        unlockedHeaders,
			    lockedHeaders
		    ];
	    },
    	
	    // Overridden to support multiple header rows
	    updateHeaders : function(){
		    var hd = this.renderHeaders();
		    this.innerHd.firstChild.innerHTML = hd[0];
		    this.innerHd.firstChild.style.width = this.getOffsetWidth();
    		
		    Ext.fly(this.innerHd.firstChild).select('>table').setWidth(this.getTotalWidth());
		    this.lockedInnerHd.firstChild.innerHTML = hd[1];
		    var lw = this.getLockedWidth();
		    this.lockedInnerHd.firstChild.style.width = lw;
		    Ext.fly(this.lockedInnerHd.firstChild).select('table').setWidth(lw);
	    },

	    initTemplates : function(){
		    var ts = this.templates || {};
		    if(!ts.master){
			    ts.master = this.masterTpl;
		    }

            // Ext 3.3
            if (!ts.masterTpl) {
                ts.masterTpl = this.masterTpl;
            }
		    this.templates = ts;
		    Sch.AbstractSchedulerView.prototype.initTemplates.call(this);
	    },
    	
        updateGroupStyles: function() {
		    var lockedTables = this.lockedHd.query('.x-grid3-header-offset > table'),
                unlockedTables = this.mainHd.query('.x-grid3-header-offset > table'), 
		        headerRows = this.headerRows || [], 
                headerConfig = this.grid.getHeaderConfig(),
                hasRightSectionColumns = this.rightColumns.length > 0,
			    unlockedWidth = (this.cm.getTotalWidth() - this.cm.getTotalLockedWidth()) + 'px',
		        lockedWidth = this.getLockedWidth();
    		
		    for (var row = 0; row < lockedTables.length; row++) {
			    lockedTables[row].style.width = lockedWidth;
			    unlockedTables[row].style.width = unlockedWidth;
    			
			    if (row < lockedTables.length - 1) {
				    var rowPos = unlockedTables[row].className.match(this.rowPosRe)[1],
                        cells = unlockedTables[row].firstChild.firstChild.childNodes,
                        width;
				    cells[0].style.width = lockedWidth;
                    
                    for (var i = 0; i < cells.length; i++) {
                        var group = headerRows[rowPos][i+1];
                        if (hasRightSectionColumns && i === cells.length - 1) {
                            cells[i].style.width = this.getRightGroupWidth() + 'px';
                        } else {
                            width = this.getHeaderGroupCellWidth(group.start, group.end, headerConfig[rowPos].unit, headerConfig[rowPos].increment || 1);
                            cells[i].style.width = width + 'px';
                        }
                    }
			    }
		    }
	    },
        
        
        translateToScheduleCoordinate : function(x) {
            return x - this.el.getX() - (Ext.isBorderBox ? -1 : this.lockedBorderWidth) + this.scroller.getScroll().left;
        }
    };
    
     
    Sch.AbstractLockingSchedulerView = Ext.extend(Sch.AbstractSchedulerView, Ext.apply({
        init: function (grid) {
            if (!this.forceFit && this.autoExpandTimeColumns) {
                this.refresh = this.refresh.createInterceptor(function (headersToo) { 
                     if (this.getScheduleSectionWidth() < this.getAvailableWidthForSchedule()) {
                         this.fitColumns(true);
                         if (!headersToo) {
                            this.updateHeaders();
                         }
                     }               
                });
            }
            
            Sch.AbstractLockingSchedulerView.superclass.init.apply(this, arguments);
        }
    }, abstractPrototype));

    
    
    Sch.LockingSchedulerView = Ext.extend(Sch.HorizontalSchedulerView, Ext.apply({
        init: function (grid) {
            if (!this.forceFit && this.autoExpandTimeColumns) {
                this.refresh = this.refresh.createInterceptor(function (headersToo) { 
                     if (this.getScheduleSectionWidth() < this.getAvailableWidthForSchedule()) {
                         this.fitColumns(true);
                         if (!headersToo) {
                            this.updateHeaders();
                         }
                     }               
                });
            }
            
            Sch.LockingSchedulerView.superclass.init.apply(this, arguments);
        },
    	
	    doRender : function(cs, rs, ds, startRow, colCount, stripe){
		    var ts = this.templates, ct = ts.cell, rt = ts.row, last = colCount-1,
			    tstyle = 'width:'+this.getTotalWidth()+';height:' + this.rowHeight + 'px;',
			    lstyle = 'width:'+this.getLockedWidth()+';height:' + this.rowHeight + 'px;',
			    buf = [], lbuf = [], cb, lcb, c, p = {}, rp = {}, r, events, totalScheduleWidth = this.getScheduleSectionWidth();
			    
		    for(var j = 0, len = rs.length; j < len; j++){
			    r = rs[j]; cb = []; lcb = [];
			    var rowIndex = (j+startRow),
			        resId = r.get('Id');
    			
			    // @Modification: Events for current row
                events = this.es.data.filterBy(function(brec) {
                    return brec.data.ResourceId == resId;
                });
                
			    for(var i = 0; i < colCount; i++){
				    c = cs[i];
				    p.id = c.id;
				    p.css = (i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : '')) +
                        (this.cm.config[i].cellCls ? ' ' + this.cm.config[i].cellCls : '');
				    p.attr = p.cellAttr = '';
				    // @Modification: Added events to param list
                    p.value = c.renderer.call(c.scope || c, r.data[c.name], p, r, rowIndex, i, ds, events);
				    p.style = c.style;
				    if(Ext.isEmpty(p.value)){
					    p.value = '&#160;';
				    }
				    if(this.markDirty && r.dirty && Ext.isDefined(r.modified[c.name])){
					    p.css += ' x-grid3-dirty-cell';
				    }
				    if(c.locked){
					    lcb[lcb.length] = ct.apply(p);
				    }else{
					    cb[cb.length] = ct.apply(p);
				    }
			    }
			    var alt = [];
			    if(stripe && ((rowIndex+1) % 2 === 0)){
				    alt[0] = 'x-grid3-row-alt';
			    }
			    if(r.dirty){
				    alt[1] = ' x-grid3-dirty-row';
			    }
			    rp.cols = colCount;
			    if(this.getRowClass){
				    alt[2] = this.getRowClass(r, rowIndex, rp, ds);
			    }
			    rp.alt = alt.join(' ');
			    rp.cells = cb.join('');
			    rp.tstyle = tstyle;
			    buf[buf.length] = rt.apply(rp);
			    rp.cells = lcb.join('');
			    rp.tstyle = lstyle;
			    lbuf[lbuf.length] = rt.apply(rp);
		    }
		    return [buf.join(''), lbuf.join('')];
	    }
    }, abstractPrototype));

    // Adaptations for GridView changes in Ext 3.3
    if (Ext.getMajorVersion() >= 3 && Ext.getMinorVersion() >= 3) {
        Ext.override(Sch.LockingSchedulerView, {
	    
	        refreshRow: function(record){
                var store = this.ds, 
                    colCount = this.cm.getColumnCount(), 
                    columns = this.getColumnData(), 
                    last = colCount - 1, 
                    cls = ['x-grid3-row'], 
                    rowParams = {
                        tstyle: String.format("width: {0};height:{1}px;", this.getTotalWidth(), this.rowHeight)
                    }, 
                    lockedRowParams = {
                        tstyle: String.format("width: {0};height:{1}px;", this.getLockedWidth(), this.rowHeight)
                    }, 
                    colBuffer = [], 
                    lockedColBuffer = [], 
                    cellTpl = this.templates.cell, 
                    rowIndex, 
                    row, 
                    lockedRow, 
                    column, 
                    meta, 
                    css, 
                    i;
            
                if (Ext.isNumber(record)) {
                    rowIndex = record;
                    record = store.getAt(rowIndex);
                } else {
                    rowIndex = store.indexOf(record);
                }
            
                if (!record || rowIndex < 0) {
                    return;
                }
            
                // @Modification: Events for current row
                var resId = record.get('Id'),
                    events = this.es.data.filterBy(function(brec) {
                    return brec.data.ResourceId == resId;
                });
            
                for (i = 0; i < colCount; i++) {
                    column = columns[i];
                
                    if (i === 0) {
                        css = 'x-grid3-cell-first';
                    } else {
                        css = (i == last) ? 'x-grid3-cell-last ' : '';
                    }
                
                    meta = {
                        id: column.id,
                        style: column.style,
                        css: css,
                        attr: "",
                        cellAttr: ""
                    };
                
                    meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store, events);
                
                    if (Ext.isEmpty(meta.value)) {
                        meta.value = ' ';
                    }
                
                    if (this.markDirty && record.dirty && typeof record.modified[column.name] != 'undefined') {
                        meta.css += ' x-grid3-dirty-cell';
                    }
                
                    if (column.locked) {
                        lockedColBuffer[i] = cellTpl.apply(meta);
                    } else {
                        colBuffer[i] = cellTpl.apply(meta);
                    }
                }
            
                row = this.getRow(rowIndex);
                row.className = '';
                lockedRow = this.getLockedRow(rowIndex);
                lockedRow.className = '';
            
                if (this.grid.stripeRows && ((rowIndex + 1) % 2 === 0)) {
                    cls.push('x-grid3-row-alt');
                }
            
                if (this.getRowClass) {
                    rowParams.cols = colCount;
                    cls.push(this.getRowClass(record, rowIndex, rowParams, store));
                }
            
                // Unlocked headerRows
                this.fly(row).addClass(cls).setStyle(rowParams.tstyle);
                rowParams.cells = colBuffer.join("");
                row.innerHTML = this.templates.rowInner.apply(rowParams);
            
                // Locked headerRows
                this.fly(lockedRow).addClass(cls).setStyle(lockedRowParams.tstyle);
                lockedRowParams.cells = lockedColBuffer.join("");
                lockedRow.innerHTML = this.templates.rowInner.apply(lockedRowParams);
                lockedRow.rowIndex = rowIndex;
                this.syncRowHeights(row, lockedRow);  
                this.fireEvent('rowupdated', this, rowIndex, record);
            }
        });
    }

    // Overridden to fix scope issue: http://www.sencha.com/forum/showthread.php?120002-3.3.1-Ext.ux.grid.LockingGridView-does-not-include-scope-in-getColumnData.&p=556348#post556348
    Ext.override(Sch.LockingSchedulerView, {
        getColumnData : function(){
	        var cs = [], cm = this.cm, colCount = cm.getColumnCount();
	        for(var i = 0; i < colCount; i++){
		        var name = cm.getDataIndex(i);
		        cs[i] = {
                    scope : cm.config[i].scope,
                    name : name || '', 
			        renderer : cm.getRenderer(i),
			        id : cm.getColumnId(i),
			        style : this.getColumnStyle(i),
			        locked : cm.isLocked(i)
		        };
	        }
	        return cs;
        }
    });
})();



Ext.ns('Sch.plugins');


Sch.plugins.DragSelector = Ext.extend(Ext.dd.DragTracker, {
    
    constructor : function(cfg) {
        cfg = cfg || {};

        Ext.applyIf(cfg,  {
            onBeforeStart: this.onBeforeStart,
            onStart: this.onStart,
            onDrag: this.onDrag,
            onEnd: this.onEnd
        });
        
        Sch.plugins.DragSelector.superclass.constructor.call(this, cfg);
    },

    init : function(scheduler){
        scheduler.on({
            afterrender : this.onSchedulerRender, 
            destroy : this.onSchedulerDestroy, 
            scope : this
        });

        this.scheduler = scheduler;
    },
    
    onBeforeStart : function (e){
        return e.ctrlKey;
    },

    onStart : function (e){
        var scheduler = this.scheduler,
            view = scheduler.getView();

        if (!this.proxy) {
            this.proxy = scheduler.el.createChild({ cls:'sch-drag-selector x-view-selector' });
        } else {
            this.proxy.show();
        }
        this.bodyRegion = view.mainBody.getRegion();

        var eventData = [];
        view.mainBody.select(scheduler.eventSelector).each(function(el){
            eventData[eventData.length] = {
                region : el.getRegion(),
                node : el.dom
            };
        }, this);

        this.eventData = eventData;

        this.sm.clearSelections();
    },

    onDrag : function (e){
        var tracker = this,
            startXY = tracker.startXY,
            sm = this.sm,
            eventData = this.eventData,
            dragRegion = this.dragRegion,
            xy = tracker.getXY(),
            x = Math.min(startXY[0], xy[0]),
            y = Math.min(startXY[1], xy[1]),
            w = Math.abs(startXY[0] - xy[0]),
            h = Math.abs(startXY[1] - xy[1]),
            i, ev, len, sel;

        dragRegion.left = x;
        dragRegion.top = y;
        dragRegion.right = x + w;
        dragRegion.bottom = y + h;

        dragRegion.constrainTo(this.bodyRegion);
        this.proxy.setRegion(dragRegion);

        for(i = 0, len = eventData.length; i < len; i++){
            ev = eventData[i]; 
            sel = dragRegion.intersect(ev.region);

            if(sel && !ev.selected) {
                ev.selected = true;
                sm.select(ev.node, true);
            }else if(!sel && ev.selected) {
                ev.selected = false;
                sm.deselect(ev.node);
            }
        }
    },

    onEnd : function (e){
        if(this.proxy){
            this.proxy.setDisplayed(false);
        }
    },

    onSchedulerRender : function (s){
        this.dragRegion = new Ext.lib.Region(0,0,0,0);
        this.sm = s.getSelectionModel();
        
        this.initEl(s.view.mainBody);
    },

    onSchedulerDestroy : function() {
        Ext.destroy(this.proxy);

        this.destroy();
    }
});

  


Ext.ns('Sch.plugins');


Sch.plugins.EventEditor = Ext.extend(Ext.form.FormPanel, {
    
    
    durationText : 'hrs',
    
    
    saveText: 'Save',

    
    deleteText: 'Delete',
    
    
    cancelText: 'Cancel',

    
    hideOnBlur : true,
     
    
     
    
                 
    
    timeConfig : {
         allowBlank:false,
         editable : false,
         forceSelection : true,
         value : '11:00'    // Dummy value required for IE
    },
    
    
    dateConfig: {
        allowBlank:false
    },

    
    
    
    dateFormat:'Y-m-d',
    
    
    timeFormat:'H:i',
    
    constructor : function(config) {
        this.addEvents(
            
            'beforeeventdelete',

            
            'beforeeventsave'
        );

        Sch.plugins.EventEditor.superclass.constructor.apply(this, arguments);
    },

    
    show : function(eventRecord, alignToEl) {
        
        // Only show delete button if the event belongs to a store
        if (this.deleteButton) {
            this.deleteButton.setVisible(!!eventRecord.store);
        }

        this.eventRecord = eventRecord;
        
        // Manually set the duration field value
        var duration = Sch.util.Date.getDurationInHours(eventRecord.get('StartDate'), eventRecord.get('EndDate'));
        this.durationField.setValue(duration);
        
        // Load form panel fields
        this.getForm().loadRecord(eventRecord);
        alignToEl = alignToEl || this.scheduler.view.getElementFromEventRecord(eventRecord);
        this.el.alignTo(alignToEl, 'bl', this.getConstrainOffsets(alignToEl));
        this.expand();
    },
    
    // Override this to add support for constraining the editor panel to the viewport or scheduler
    getConstrainOffsets : function(eventEl) {
        return [0, 0];
    },
    
    cls : 'sch-eventeditor',
    layout : 'border',
    border : false,
    
    onSaveClick : function() {
        var formPanel = this,
            record = formPanel.eventRecord,
            form = formPanel.getForm();
        
        if (form.isValid() && this.fireEvent('beforeeventsave', this, record) !== false) {

            var start = formPanel.startDateField.getValue(),
                hours = formPanel.durationField.getValue();

            if (start && hours >= 0) {
                end = Sch.util.Date.add(start, Sch.util.Date.MINUTE, hours * 60);
            } else {
                return;
            }

            var values = form.getValues();

            record.beginEdit();
            form.updateRecord(record);
            record.set('StartDate', start);
            record.set('EndDate', end);
            record.endEdit();

            // Check if this is a new record
            if (!this.eventRecord.store) {
                if (this.scheduler.fireEvent('beforeeventadd', this.scheduler, record) !== false) {
                    this.eventStore.add(record);
                }
            }
            formPanel.collapse();
        }
    },

    onDeleteClick : function() {
        if (this.fireEvent('beforeeventdelete', this, this.eventRecord) !== false) {
            this.eventStore.remove(this.eventRecord);
        }
        this.collapse();
    },

    onCancelClick : function() {
        this.collapse();
    },

    buildButtons : function() {
        this.saveButton = new Ext.Button({
            text : this.saveText,
            scope : this,
            handler : this.onSaveClick
        });

        this.deleteButton = new Ext.Button({
            text : this.deleteText,
            scope : this,
            handler : this.onDeleteClick
        });
        
        this.cancelButton = new Ext.Button({
            text : this.cancelText,
            scope : this,
            handler : this.onCancelClick
        });

        return [this.saveButton, this.deleteButton, this.cancelButton];
    },

    buildDurationFields : function() {
        this.startDateField = new Ext.ux.form.DateTime({
            name : 'StartDate',
            x:10,
            y:7,
            width:160,
            timeFormat:this.timeFormat,
            timeWidth : 60,
            timeConfig: this.timeConfig,
            dateFormat:this.dateFormat,
            dateConfig : this.dateConfig
        });
                    
        this.durationField = new Ext.ux.form.SpinnerField({
            x:180,
            y:7,
            width:55,
            minValue : 0,
            allowNegative:false
        });
                    
        this.durationLabel = new Ext.form.Label({
            y:7,
            x:240,
            xtype : 'label',
            text : this.durationText
        });

        return [this.startDateField, this.durationField, this.durationLabel];
    },

    initComponent : function() {
        
        if (!this.fieldsPanelConfig) throw 'Must define a fieldsPanelConfig property';
        
        this.fieldsPanelConfig.region = 'center';

        Ext.apply(this, {
            fbar : this.buildButtons(),
            items : [{
                region : 'north',
                layout : 'absolute',
                height :35,
                border : false,
                cls : 'sch-eventeditor-timefields',
                items : this.buildDurationFields()
            },
            this.fieldsPanelConfig]
        });
        Sch.plugins.EventEditor.superclass.initComponent.call(this);
    },
    
    init : function(scheduler) {
        this.scheduler = scheduler;
        this.eventStore = scheduler.getEventStore();

        this.scheduler.on({
            eventdblclick : this.onEventDblClick, 
            afterrender : this.onSchedulerRender,
            destroy : this.onSchedulerDestroy,
            dragcreateend : this.onDragCreateEnd, 
            scope : this
        });
        
        this.scheduler.registerEventEditor(this);
    },
    
    onEventDblClick : function(g, evtRecord) {
        this.show(evtRecord);
    },
    
    onSchedulerRender : function() {
        this.render(Ext.getBody());

        // Collapse after render, otherwise rendering is messed up
        this.collapse();
         
        if (this.hideOnBlur) {
            // Hide when clicking outside panel
            this.mon(Ext.getBody(), 'mousedown', this.onMouseDown, this);
        }
    },

    onMouseDown : function(e){
        var sd = this.startDateField;
                        
        if (!this.collapsed && 
            !e.within(this.getEl()) &&
            (!sd.tf.view || !e.within(sd.tf.view.el)) &&
            (!sd.df.menu || !e.within(sd.df.menu.el))) {        // HACK, reading private/undocumented property 'menu'
            this.collapse(false);
        }
    },

    onSchedulerDestroy : function() {
        this.scheduler.un('eventdblclick', this.onEventDblClick, this);
    },

    onDragCreateEnd : function(s, eventRecord) {
        if (!this.dragProxyEl && this.scheduler.dragCreator) {
            this.dragProxyEl = this.scheduler.dragCreator.getProxy();
        }

        // Call scheduler template method
        this.scheduler.onEventCreated(eventRecord);

        this.show(eventRecord, this.dragProxyEl);
    },

    // Always hide drag proxy on collapse
    onCollapse : function() {
        var dpEl = this.dragProxyEl;
        if (dpEl) {
            dpEl.hide();
        }
        Sch.plugins.EventEditor.superclass.onCollapse.apply(this, arguments);
    }
});



Ext.ns('Sch.plugins');


Sch.AbstractTimeSpan = Ext.extend(Ext.util.Observable, {
    
    disabled : false,
    
    setDisabled : function(disabled) {
        if (disabled) {
            this.removeElements();
        }
        
        this.disabled = disabled;
    },

    constructor : function(cfg) {
        // unique css class to be able to identify only the zones belonging to this plugin
        this.cls = this.cls || ('sch-timespangroup-' + Ext.id());
        
        Ext.apply(this, cfg);
        Sch.AbstractTimeSpan.superclass.constructor.apply(this, arguments);
    },
     
    // private
    getElements : function() {
        return this.containerEl.select('.' + this.cls);
    },
    
    // private
    removeElements : function() {
        return this.getElements().remove();
    },
    
    init:function(scheduler) {
        this.scheduler = scheduler;
    
        if (!this.store) {
            throw "Without a store, there's not much use for this plugin";
        }
        scheduler.on({
            render : this.onAfterRender, 
            destroy : this.onDestroy, 
            scope : this
        });
    },
    
    onAfterRender : function (scheduler) {
        var v = scheduler.getView();
        
        this.containerEl = v.scroller;
        this.schedulerBodyEl = v.mainBody;
        
        this.store.on({
            load : this.renderElements,
            datachanged : this.renderElements, 
            add : this.renderElements, 
            remove : this.renderElements, 
            update : this.renderElements, 
            clear : this.renderElements, 
            scope : this
        });
            
        v.on({
            refresh : this.renderElements, 
            rowremoved : this.refreshElements,
            rowsinserted : this.refreshElements,
            scope : this
        });

        scheduler.on({
            viewchange : this.renderElements, 
            afterrender : this.renderElements,
            scope : this
        });

        // HACK
        if ('togglegroup' in v.events) {
            v.on('togglegroup', this.refreshElements, this);
        }
        
        // HACK
        if ('togglerow' in v.events) {
            v.on('togglerow', this.refreshElements, this);
        }
        scheduler.mon(scheduler.getColumnModel(), 'hiddenchange', this.renderElements, this);
    },
    
    renderElements : function() {
        // Don't render anything if the host component is not ready
        if (this.disabled || !this.scheduler.viewReady) return;
        
        // Defer to make sure rendering is not delayed by this plugin
        this.renderElementsInternal.defer(10, this);
    },
    
    renderElementsInternal : function() {
        this.removeElements();
        
        var h = this.schedulerBodyEl.getHeight(),
            start = this.scheduler.getStart(), 
            end = this.scheduler.getEnd(),
            data = this.getElementData(start, end, h);
        
        for (var i = 0, l = data.length; i < l; i++) {
            // Add an identifier to be able to 
            data[i].uniquecls = this.cls;
        }
        this.template.insertFirst(this.containerEl, data);
    },
    
    getElementData : function(viewStart, viewEnd, containerHeight) {
       throw 'Abstract method call';
    },

    // Refresh the height of the rendered elements in the DOM
    refreshElements : function() {
        var h = this.schedulerBodyEl.getHeight();
        
        this.getElements().setHeight(h);
    },

    onDestroy : function() {
        this.store.destroy();
    }
}); 



Ext.ns('Sch.plugins');


Sch.plugins.Lines = Ext.extend(Sch.AbstractTimeSpan, {    
    
    
    dateFormat : 'y-m-d G:i',
    
    init:function(scheduler) {
        if (!this.template) {
            this.template = new Ext.XTemplate(
                '<tpl for=".">',
                    '<div title="{[values.Date.format("' + this.dateFormat + '")]} - {[values.Text || ""]}" class="sch-timeline {uniquecls} {Cls}" style="left:{left}px;top:{top}px;height:{height}px;width:{width}px"></div>',
                '</tpl>'
            );       
        }
        
        Sch.plugins.Lines.superclass.init.call(this, scheduler);
    },
    
    getElementData : function(viewStart, viewEnd) {
        var s = this.store,
            scheduler = this.scheduler,
            view = scheduler.getView(),
            rs = s.getRange(),
            data = [],
            r, date, region, width;

        for (var i = 0, l = s.getCount(); i < l; i++) {
            r = rs[i];
            date = r.get('Date');

            if (date.between(viewStart, viewEnd)) {
                region = view.getTimeSpanRegion(date);
                
                data[data.length] = Ext.apply({
                    left : region.left,
                    top : region.top,
                    width : Math.max(1, region.right-region.left),
                    height : Math.max(1, region.bottom - region.top)
                }, r.data);
            }
        }
        return data;
    }
}); 



Ext.ns('Sch.plugins');


Sch.plugins.Zones = Ext.extend(Sch.AbstractTimeSpan, {
   
    init:function(scheduler) {
        if (!this.template) {
            this.template = new Ext.XTemplate(
                '<tpl for=".">',
                    '<div class="sch-zone {uniquecls} {Cls}" style="left:{left}px;top:{top}px;height:{height}px;width:{width}px"></div>',
                '</tpl>'
            );
        }
        Sch.plugins.Zones.superclass.init.call(this, scheduler);
    },

    getElementData : function(viewStart, viewEnd) {
        var s = this.store,
            scheduler = this.scheduler,
            view = scheduler.getView(),
            rs = s.getRange(),
            data = [],
            r, spanStart, spanEnd, region;

        for (var i = 0, l = s.getCount(); i < l; i++) {
            r = rs[i];
            spanStart = r.get('StartDate');
            spanEnd = r.get('EndDate');

            if (Sch.util.Date.intersectSpans(spanStart, spanEnd, viewStart, viewEnd)) {
                region = view.getTimeSpanRegion(Sch.util.Date.max(spanStart, viewStart), Sch.util.Date.min(spanEnd, viewEnd));
                
                data[data.length] = Ext.apply({
                    left : region.left,
                    top : region.top,
                    width : region.right-region.left,
                    height : region.bottom - region.top
                }, r.data);
            }
        }
        return data;
    }
}); 



Ext.ns('Sch.plugins');
 

Sch.plugins.CurrentTimeLine = function(config, grid) {
    Ext.apply(this, config);
    this.grid = grid;
    Sch.plugins.CurrentTimeLine.superclass.constructor.call(this);
};
 
Ext.extend(Sch.plugins.CurrentTimeLine, Sch.plugins.Lines, {
    
    
    tooltipText : 'Current time',
    
    
    updateInterval : 60000,
    
    
    autoUpdate : true,
    
    init : function(cmp) {
        var store = new Ext.data.JsonStore({
            fields : ['Date', 'Cls', 'Text'],
            data : [{Date : new Date(), Cls : 'sch-todayLine', Text : this.tooltipText}]
        });
            
        if (this.autoUpdate) {
            this.runner = new Ext.util.TaskRunner();
            this.runner.start({
                run: function() {
                    store.getAt(0).set('Date', new Date());
                },
                interval: this.updateInterval 
            });
        }

        cmp.on('destroy', this.onHostDestroy, this);
        
        this.store = store;
        Sch.plugins.CurrentTimeLine.superclass.init.apply(this, arguments);
    },

    onHostDestroy : function() {
        if (this.runner) {
            this.runner.stopAll();
        }
    }
}); 



Ext.ns('Sch.plugins');


Sch.plugins.Pan = Ext.extend(Object, {
    
    enableVerticalPan : true,
    
    
    hideScrollbar : false,
    
    constructor : function(config) {
        Ext.apply(this, config);
    },

    init : function(grid) {
        grid.on('render', this.onRender, this);
    },

    onRender: function(grid) {
        this.eventSelector = grid.eventSelector;
        this.panEl = grid.getView().scroller;
        grid.mon(this.panEl, 'mousedown', this.onMouseDown, this , { delegate : '.x-grid3-row' });
        
        if (this.hideScrollbar) {
            this.panEl.setStyle('overflow', 'hidden');
        }
    },

    onMouseDown: function(e, t) {
        if (!e.getTarget(this.eventSelector)) {
            this.mouseX = e.getPageX();
            this.mouseY = e.getPageY();
            Ext.getBody().on('mousemove', this.onMouseMove, this);
            Ext.getDoc().on('mouseup', this.onMouseUp, this);
        }
    },

    onMouseMove: function(e) {
        e.stopEvent();
        
        var x = e.getPageX(),
            y = e.getPageY(),
            xDelta = x - this.mouseX,
            yDelta = y - this.mouseY;
            
        this.panEl.scrollTo('left', this.panEl.dom.scrollLeft - xDelta);
        this.mouseX = x;
        this.mouseY = y;
        
        if (this.enableVerticalPan) {
            this.panEl.scrollTo ('top', this.panEl.dom.scrollTop - yDelta);
        }
    },

    onMouseUp: function(e) {
        Ext.getBody().un('mousemove', this.onMouseMove, this);
        Ext.getDoc().un('mouseup', this.onMouseUp, this);
    }   
});



Ext.ns('Sch');


Sch.ColumnFactory = {
    
    // Default renderer method if no renderer is supplied in the header config
    defaultRenderer : function(start, end, dateFormat) {
        return start.format(dateFormat);
    },

    // Method generating the column config array for the time columns 
    createColumns : function (timeAxis, headerConfig, defaults) {
        if (!timeAxis || !headerConfig) {
            throw 'Invalid parameters passed to createColumns';
        }
        
        var columns = [],
            lowestHeader = headerConfig.bottom || headerConfig.middle,
            ticks = timeAxis.getTicks(),
            colConfig;
            
        for (var i = 0, l = ticks.length; i < l; i++) {
            colConfig = {
                align : lowestHeader.align || 'center',
                headerCls : ''
            };
            
            if (lowestHeader.renderer) {
                colConfig.header = lowestHeader.renderer.call(lowestHeader.scope || this, ticks[i].start, ticks[i].end, colConfig, i);
            } else {
                colConfig.header = this.defaultRenderer(ticks[i].start, ticks[i].end, lowestHeader.dateFormat);
            }

            columns[columns.length] = new Sch.TimeColumn(Ext.apply(colConfig, defaults));
        }

        var headerRows = this.createHeaderRows(timeAxis, headerConfig);

        return {
            columns : columns,
            headerRows : headerRows
        };
    },

    
    createHeaderRows : function (timeAxis, headerConfig) {
        var rows = {};

        if (headerConfig.top) {
            var topRow;
            if (headerConfig.top.cellGenerator) {
                topRow = headerConfig.top.cellGenerator.call(this, timeAxis.getStart(), timeAxis.getEnd());
            } else {
                topRow = this.createHeaderRow(timeAxis, headerConfig.top);
            }
            rows.top = topRow;
        }

        if (headerConfig.bottom) {
            var middleRow;
            if (headerConfig.middle.cellGenerator) {
                middleRow = headerConfig.middle.cellGenerator.call(this, timeAxis.getStart(), timeAxis.getEnd());
            } else {
                middleRow = this.createHeaderRow(timeAxis, headerConfig.middle);
            }
            rows.middle = middleRow;
        }

        return rows;
    },

    // private
    createHeaderRow: function(timeAxis, headerConfig) {
        var cells = [],
            colConfig,
            start = timeAxis.getStart(),
            end = timeAxis.getEnd(),
            totalDuration = end - start,
            cols = [],
            dt = start,
            i = 0,
            cfg,
            align = headerConfig.align || 'center',
            intervalEnd;
        
        while (dt < end) {
            intervalEnd =  Sch.util.Date.min(timeAxis.getNext(dt, headerConfig.unit, headerConfig.increment || 1), end);
            
            colConfig = {
                align : align,
                start : dt,
                end : intervalEnd,
                headerCls : ''
            };
                
            if (headerConfig.renderer) {
                colConfig.header = headerConfig.renderer.call(headerConfig.scope || this, dt, intervalEnd, colConfig, i);
            } else {
                colConfig.header = this.defaultRenderer(dt, intervalEnd, headerConfig.dateFormat, colConfig, i);
            }
               
            cells.push(colConfig);
            dt = intervalEnd;
            i++;
        }

        return cells;
    }
}; 


Ext.ns('Sch');
 

Sch.ClockTemplate = function() {
    var toRad = Math.PI / 180,
        cos = Math.cos,
        sin = Math.sin,
        minuteHeight = 7,
        minuteTop = 2,
        minuteLeft = 10,
        hourHeight = 6,
        hourTop = 3,
        hourLeft = 10;

    function getHourStyleIE(degrees) {
        var rad = degrees * toRad,
            cosV = cos(rad),
            sinV = sin(rad),
            y = hourHeight * sin((90-degrees)*toRad),
            x =hourHeight * cos((90-degrees)*toRad),
            topAdjust = Math.min(hourHeight, hourHeight - y),
            leftAdjust = degrees > 180 ? x : 0,
            matrixString = "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11 = " + cosV + ", M12 = " + (-sinV) + ", M21 = " + sinV + ", M22 = " + cosV + ")";
        
        return String.format("filter:{0};-ms-filter:{0};top:{1}px;left:{2}px;", matrixString, topAdjust+hourTop, leftAdjust+hourLeft);
    }

    function getMinuteStyleIE(degrees) {
        var rad = degrees * toRad,
            cosV = cos(rad),
            sinV = sin(rad),
            y = minuteHeight * sin((90-degrees)*toRad),
            x = minuteHeight * cos((90-degrees)*toRad),
            topAdjust = Math.min(minuteHeight, minuteHeight - y),
            leftAdjust = degrees > 180 ? x : 0,
            matrixString = "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11 = " + cosV + ", M12 = " + (-sinV) + ", M21 = " + sinV + ", M22 = " + cosV + ")";
        
        return String.format("filter:{0};-ms-filter:{0};top:{1}px;left:{2}px;", matrixString, topAdjust+minuteTop, leftAdjust+minuteLeft);
    }

    function getStyle(degrees) {
        return String.format("transform:rotate({0}deg);-moz-transform: rotate({0}deg);-webkit-transform: rotate({0}deg);-o-transform:rotate({0}deg);", degrees);
    }

    return new Ext.XTemplate(
        '<div class="sch-clockwrap {cls}">' +
            '<div class="sch-clock">' +
                '<div class="sch-hourIndicator" style="{[this.getHourStyle((values.date.getHours()%12) * 30)]}">{[Date.monthNames[values.date.getMonth()].substr(0,3)]}</div>' +
                '<div class="sch-minuteIndicator" style="{[this.getMinuteStyle(values.date.getMinutes() * 6)]}">{[values.date.getDate()]}</div>' +
            '</div>' +
            '<span class="sch-clock-text">{text}</span>' +
        '</div>',
        {
            compiled : true,
            disableFormats : true,

            getMinuteStyle : Ext.isIE ? getMinuteStyleIE : getStyle,
            getHourStyle : Ext.isIE ? getHourStyleIE : getStyle
        }
    );
}; 



Ext.ns('Sch');


Sch.PresetManager = Ext.apply(new Ext.util.MixedCollection(), {   
    
    
    registerPreset : function(name, cfg) {
        if (cfg) {
            var headerConfig = cfg.headerConfig;
            
            // Make sure date "unit" constant specified in the preset are resolved
            for (var o in headerConfig) {
                if (headerConfig.hasOwnProperty(o)) {
                    if (Sch.util.Date[headerConfig[o].unit]) {
                        headerConfig[o].unit = Sch.util.Date[headerConfig[o].unit.toUpperCase()];
                    }
                }
            }

            if (cfg.timeResolution && Sch.util.Date[cfg.timeResolution.unit]) {
                cfg.timeResolution.unit = Sch.util.Date[cfg.timeResolution.unit.toUpperCase()];
            }

            if (cfg.shiftUnit && Sch.util.Date[cfg.shiftUnit]) {
                cfg.shiftUnit = Sch.util.Date[cfg.shiftUnit.toUpperCase()];
            }
        }

        if (this.isValidPreset(cfg)) {
            this.add(name, cfg);
        } else {
            throw 'Invalid preset, please check your configuration';
        }
    },

    isValidPreset : function(cfg) {
        var D = Sch.util.Date,
            valid = true,
            validUnits = [D.MILLI, D.SECOND, D.MINUTE, D.HOUR, D.DAY, D.WEEK, D.MONTH, D.QUARTER, D.YEAR];
       
        // Make sure all date "unit" constants are valid
        for (var o in cfg.headerConfig) {
            if (cfg.headerConfig.hasOwnProperty(o)) {
                valid = valid && validUnits.indexOf(cfg.headerConfig[o].unit) >= 0;
            }
        }

        if (cfg.timeResolution) {
            valid = valid && validUnits.indexOf(cfg.timeResolution.unit) >= 0;
        }

        if (cfg.shiftUnit) {
            valid = valid && validUnits.indexOf(cfg.shiftUnit) >= 0;
        }

        return valid && !this.containsKey(name);
    },

    
    getPreset : function(name) {
        return this.get(name);
    },

    
    deletePreset : function(name) {
        this.removeKey(name);
    }
}); 



Ext.ns('Sch');


Sch.TimeAxis = Ext.extend(Ext.util.Observable, {
    
    
    continuous : true,

    autoAdjust : true,

    // private
    constructor : function(config) {
        this.addEvents(
            
            'reconfigure'
        );
        
        this.tickStore = new Ext.data.JsonStore({
            fields : [{ name : 'start', type : 'date' }, { name : 'end', type : 'date' }]
        });
        
        this.tickStore.on('datachanged', function() {
            this.fireEvent('reconfigure', this);
        }, this);

        Ext.apply(this, config);
        Sch.TimeAxis.superclass.constructor.apply(this, arguments);
    },

    
    reconfigure : function(config) {
        Ext.apply(this, config); 
        var ticks = this.generateTicks(this.start, this.end, this.unit, this.increment || 1, this.mainUnit);
        this.tickStore.loadData(ticks);
    },

    
    setTimeSpan : function(start, end) {
        var ticks = this.generateTicks(start, end);
        this.tickStore.loadData(ticks);
    },

    // private
    getIncrement : function() {
        return this.increment;
    },

    
    filterBy : function(fn, scope) {
        this.continuous = false;
        this.tickStore.filterBy(function(t, index) {
            return fn.call(scope || this, t.data, index);
        }, this);

    },

    isContinuous : function() {
        return this.continuous && !this.tickStore.isFiltered();
    },

    
    clearFilter : function() {
        this.tickStore.clearFilter();
    },

    
    generateTicks : function(start, end, unit, increment) {
    
        var ticks = [],
            intervalEnd;

        unit = unit || this.unit;
        increment = increment || this.increment;
        
        if (this.autoAdjust) {
            start = this.floorDate(start || this.getStart(), false);
            end = this.ceilDate(end || Sch.util.Date.add(start, this.mainUnit, this.defaultSpan), false);
        }
       
        while (start < end) {
            intervalEnd = this.getNext(start, unit, increment);
            
            ticks.push({
                start : start,
                end : intervalEnd
            });
            start = intervalEnd;
        }
        return ticks;
    },

    
    getTickFromDate : function(date) {
       
        if (this.getStart() > date || this.getEnd() < date) {
            return -1;
        } 

        var ticks = this.tickStore.getRange(),
            tickEnd;

        for (var i = 0, l = ticks.length; i < l; i++) {
            tickEnd = ticks[i].data.end;
            if (date <= tickEnd) {
                var tickStart = ticks[i].data.start;
                    
                return i + (date > tickStart ? (date - tickStart)/(tickEnd - tickStart) : 0);
            } 
        }
        
        return -1;
    },

    
    getDateFromTick : function(tick) {
        var count = this.tickStore.getCount();

        if (tick === count){
            return this.getEnd();
        }
        
        var t = this.getAt(Math.floor(tick));

        return Sch.util.Date.add(t.start, Sch.util.Date.MILLI, (tick - Math.floor(tick)) * (t.end - t.start));
    },

    
    getAt : function(index) {
        return this.tickStore.getAt(index).data;
    },

    // private
    getCount : function() {
        return this.tickStore.getCount();
    },
    
    // private
    getTicks : function() {
        var ticks = [];
        
        this.tickStore.each(function(r) { ticks.push(r.data); });
        return ticks;
    },

    
    getStart : function() {
        return this.tickStore.getAt(0).data.start.clone();
    },

    
    getEnd : function() {
        return this.tickStore.getAt(this.tickStore.getCount()-1).data.end.clone();
    },

    // Floors the date to nearest minute increment
    // private
    roundDate : function(date) {
        var dt = date.clone(),
            relativeTo = this.getStart(),
            increment = this.resolutionIncrement;
        
        switch(this.resolutionUnit) {
            case Sch.util.Date.MILLI:    
                var milliseconds = Sch.util.Date.getDurationInMilliseconds(relativeTo, dt),
                    snappedMilliseconds = Math.round(milliseconds / increment) * increment;
                dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MILLI, snappedMilliseconds);
                break;

            case Sch.util.Date.SECOND:
                var seconds = Sch.util.Date.getDurationInSeconds(relativeTo, dt),
                    snappedSeconds = Math.round(seconds / increment) * increment;
                dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MILLI, snappedSeconds * 1000);
                break;

            case Sch.util.Date.MINUTE:
                var minutes = Sch.util.Date.getDurationInMinutes(relativeTo, dt),
                    snappedMinutes = Math.round(minutes / increment) * increment;
                dt = Sch.util.Date.add(relativeTo, Sch.util.Date.SECOND, snappedMinutes * 60);
                break; 

            case Sch.util.Date.HOUR:
                var nbrHours = Sch.util.Date.getDurationInHours(this.getStart(), dt),
                    snappedHours = Math.round(nbrHours / increment) * increment;
                dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MINUTE, snappedHours * 60);
                break;

            case Sch.util.Date.DAY:
                var nbrDays = Sch.util.Date.getDurationInDays(relativeTo, dt),
                    snappedDays = Math.round(nbrDays / increment) * increment;
                dt = Sch.util.Date.add(relativeTo, Sch.util.Date.HOUR, snappedDays * 24);
                break;

            case Sch.util.Date.WEEK:
                dt.clearTime();

                var distanceToWeekStartDay = dt.getDay() - this.weekStartDay,
                    toAdd;
                    
                if (distanceToWeekStartDay < 0) {
                    distanceToWeekStartDay = 7 + distanceToWeekStartDay;
                }

                if (Math.round(distanceToWeekStartDay/7) === 1) {
                    toAdd = 7 - distanceToWeekStartDay;
                } else {
                    toAdd = -distanceToWeekStartDay;
                }

                dt = Sch.util.Date.add(dt, Sch.util.Date.DAY, toAdd);
                break;

            case Sch.util.Date.MONTH:
                var nbrMonths = Sch.util.Date.getDurationInMonths(relativeTo, dt) + (dt.getDate() / dt.getDaysInMonth()),
                    snappedMonths = Math.round(nbrMonths / increment) * increment;
                dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MONTH, snappedMonths);
                break;

            case Sch.util.Date.QUARTER:
                dt.clearTime();
                dt.setDate(1);
                dt = Sch.util.Date.add(dt, Sch.util.Date.MONTH, 3 - (dt.getMonth() % 3));
                break;

            case Sch.util.Date.YEAR:
                var nbrYears = Sch.util.Date.getDurationInYears(relativeTo, dt),
                    snappedYears = Math.round(nbrYears / increment) * increment;
                dt = Sch.util.Date.add(relativeTo, Sch.util.Date.YEAR, snappedYears);
                break;

        }
        return dt;
    },
    
    // Floors a date to the current resolution
    // private
    floorDate : function(date, relativeToStart) {
        relativeToStart = relativeToStart !== false;
        
        var dt = date.clone(),
            relativeTo = relativeToStart ? this.getStart() : null,
            increment = this.resolutionIncrement;
            
        switch(relativeToStart ? this.resolutionUnit : this.mainUnit) {
            case Sch.util.Date.MILLI:    
                if (relativeToStart) {
                    var milliseconds = Sch.util.Date.getDurationInMilliseconds(relativeTo, dt),
                        snappedMilliseconds = Math.floor(milliseconds / increment) * increment;
                    dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MILLI, snappedMilliseconds);
                }
                break;

            case Sch.util.Date.SECOND:
                if (relativeToStart) {
                    var seconds = Sch.util.Date.getDurationInSeconds(relativeTo, dt),
                        snappedSeconds = Math.floor(seconds / increment) * increment;
                    dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MILLI, snappedSeconds * 1000);
                } else {
                    dt.setMilliseconds(0);
                }
                break;

            case Sch.util.Date.MINUTE:
                if (relativeToStart) {
                    var minutes = Sch.util.Date.getDurationInMinutes(relativeTo, dt),
                        snappedMinutes = Math.floor(minutes / increment) * increment;
                    dt = Sch.util.Date.add(relativeTo, Sch.util.Date.SECOND, snappedMinutes * 60);
                } else {
                    dt.setSeconds(0);
                    dt.setMilliseconds(0);
                }
                break; 

            case Sch.util.Date.HOUR:
                if (relativeToStart) {
                    var nbrHours = Sch.util.Date.getDurationInHours(this.getStart(), dt),
                        snappedHours = Math.floor(nbrHours / increment) * increment;
                    dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MINUTE, snappedHours * 60);
                } else {
                    dt.setMinutes(0);
                    dt.setSeconds(0);
                    dt.setMilliseconds(0);
                }
                break;

            case Sch.util.Date.DAY:
                if (relativeToStart) {
                    var nbrDays = Sch.util.Date.getDurationInDays(relativeTo, dt),
                        snappedDays = Math.floor(nbrDays / increment) * increment;
                    dt = Sch.util.Date.add(relativeTo, Sch.util.Date.HOUR, snappedDays * 24);
                } else {
                    dt.clearTime();
                }
                break;

            case Sch.util.Date.WEEK:
                var day = dt.getDay();
                dt.clearTime();
                if (day !== this.weekStartDay) {
                    dt = Sch.util.Date.add(dt, Sch.util.Date.DAY, -(day > this.weekStartDay ? (day - this.weekStartDay) : (7 - day - this.weekStartDay)));
                }
                break;

            case Sch.util.Date.MONTH:
                if (relativeToStart) {
                    var nbrMonths = Sch.util.Date.getDurationInMonths(relativeTo, dt),
                        snappedMonths = Math.floor(nbrMonths / increment) * increment;
                    dt = Sch.util.Date.add(relativeTo, Sch.util.Date.MONTH, snappedMonths);
                } else {
                    dt.clearTime();
                    dt.setDate(1);
                }
                break;

            case Sch.util.Date.QUARTER:
                dt.clearTime();
                dt.setDate(1);
                dt = Sch.util.Date.add(dt, Sch.util.Date.MONTH, -(dt.getMonth() % 3));
                break;

            case Sch.util.Date.YEAR:
                if (relativeToStart) {
                    var nbrYears = Sch.util.Date.getDurationInYears(relativeTo, dt),
                        snappedYears = Math.floor(nbrYears / increment) * increment;
                    dt = Sch.util.Date.add(relativeTo, Sch.util.Date.YEAR, snappedYears);
                } else {
                    dt = new Date(date.getFullYear(), 0, 1);
                }
                break;

        }
        return dt;
    },

    // private
    ceilDate : function(date, relativeToStart) {
        var dt = date.clone();
        relativeToStart = relativeToStart !== false;
        
        var increment = relativeToStart ? this.resolutionIncrement : 1,
            unit = relativeToStart ? this.resolutionUnit : this.mainUnit,
            doCall = false;

        switch (unit) {
            case Sch.util.Date.DAY: 
                if (dt.getMinutes() > 0 || dt.getSeconds() > 0 || dt.getMilliseconds() > 0) {
                    doCall = true;
                }
            break;

            case Sch.util.Date.WEEK: 
                dt.clearTime();
                if (dt.getDay() !== this.weekStartDay) {
                    doCall = true;
                }
            break;

            case Sch.util.Date.MONTH: 
                dt.clearTime();
                if(dt.getDate() !== 1) {
                    doCall = true;
                }
            break;

            case Sch.util.Date.QUARTER:
                dt.clearTime();
                if(dt.getMonth() % 3 !== 0) {
                    doCall = true;
                }
            break;
            
            case Sch.util.Date.YEAR:
                dt.clearTime();
                if(dt.getMonth() !== 0 && dt.getDate() !== 1) {
                    doCall = true;
                }
            break;

            default:
            break;
        }

        if (doCall) {
            return this.getNext(dt, unit, increment);
        } else {
            return dt;
        }
    },

    // private
    getNext : function(date, unit, increment) {
        var dt = date.clone();
        
        increment = increment || 1;
        
        switch (unit) {
            case Sch.util.Date.DAY: 
                dt = Sch.util.Date.add(dt, Sch.util.Date.DAY, increment);
            break;

            case Sch.util.Date.WEEK: 
                var day = dt.getDay();
                dt = Sch.util.Date.add(dt, Sch.util.Date.DAY, (7 * (increment - 1)) + (day < this.weekStartDay ? (this.weekStartDay - day) : (7 - day + this.weekStartDay)));
            break;

            case Sch.util.Date.MONTH: 
                dt = Sch.util.Date.add(dt, Sch.util.Date.MONTH, increment);
                dt.setDate(1);
            break;

            case Sch.util.Date.QUARTER:
                dt = Sch.util.Date.add(dt, Sch.util.Date.MONTH, ((increment - 1) * 3) + (3 - (dt.getMonth() % 3)));
            break;
            
            case Sch.util.Date.YEAR:
                dt = new Date(dt.getFullYear() + increment, 0, 1);
            break;

            default:
                dt = Sch.util.Date.add(date, unit, increment);
            break;
        }

        return dt;
    },

    // private
    getResolution : function() {
        return {
            unit : this.resolutionUnit,
            increment : this.resolutionIncrement
        };
    },

    // private
    setResolution : function(unit, increment) {
        this.resolutionUnit = unit;
        this.resolutionIncrement = increment || 1;
    }
});


Ext.ns('Sch');


Sch.TimeColumn = Ext.extend(Ext.grid.Column, {
    
    align : 'center',
    menuDisabled : true,
    hideable : false,
    resizable : false,
    sortable : false,
    headerCls : '',
    hCls : ' sch-timeheader ',
    dataIndex : '_sch_not_used'
});



Ext.ns('Sch.plugins');
 

Sch.Tooltip = Ext.extend(Ext.ToolTip, {
    
    startText : 'Starts: ',
    
    endText : 'Ends: ',
    
    autoHide : true,
    anchor : 'b',
    padding: '0 3 0 0',
    trackMouse : true,
    showDelay : 0,
    hideDelay : 0,
    dismissDelay : 0,
    html : '&nbsp;',
    mouseOffset : [0, 4],

    constructor : function(config) {
        var clockTpl = new Sch.ClockTemplate();
        this.renderTo = this.target = document.body;

        if (!this.template) {
            this.template = new Ext.XTemplate(
                '<div class="{[values.valid ? "sch-tip-ok" : "sch-tip-notok"]}">',
                   '{[this.renderClock(values.startDate, values.startText, "sch-tooltip-startdate")]}',
                   '{[this.renderClock(values.endDate, values.endText, "sch-tooltip-enddate")]}',
                '</div>',
                {
                    compiled : true,
                    disableFormats : true,

                    renderClock : function(date, text, cls) {
                        return clockTpl.apply({
                            date : date, 
                            text : text,
                            cls : cls
                        });
                    }
                }
            );
        }

        Sch.Tooltip.superclass.constructor.call(this, config);
    },
    
    update : function(startDate, endDate, valid) {
        var startText = this.scheduler.getFormattedDate(startDate),
            endText = this.scheduler.getFormattedEndDate(endDate, startDate);

        // If resolution is day or greater, and end date is greater then start date
        if (this.mode === 'calendar' && endDate.getHours() === 0 && endDate.getMinutes() === 0 && 
            !(endDate.getYear() === startDate.getYear() && endDate.getMonth() === startDate.getMonth() && endDate.getDate() === startDate.getDate())) {
            endDate = Sch.util.Date.add(endDate, Sch.util.Date.DAY, -1);
        }

        Sch.Tooltip.superclass.update.call(this, this.template.apply({
            valid: valid,
            startDate: startDate,
            startText: startText,
            endText: endText,
            endDate: endDate
        }));
        this.doAutoWidth();
    },
     
    show : function(el) {
        if (Sch.util.Date.compareUnits(this.scheduler.getTimeResolution().unit, Sch.util.Date.DAY) >= 0) {
            this.mode = 'calendar';
            this.el.addClass('sch-day-resolution');
        } else {
            this.mode = 'clock';
            this.el.removeClass('sch-day-resolution');
        }
        
        Sch.Tooltip.superclass.show.call(this);

        this.origXY = this.getPositionEl().getXY();
    },

    constrainToResource : true,

    // private, overridden to constrain tooltip to one axis
    onMouseMove : function(e){
        var t = this.delegate ? e.getTarget(this.delegate) : this.triggerElement = true;
        if (t) {
            this.targetXY = e.getXY();
            if (t === this.triggerElement) {
                if(!this.hidden && this.trackMouse){
                    var targetXY = this.getTargetXY();

                    if (this.constrainToResource) {
                        this.setPagePosition([targetXY[0], this.origXY[1]]);
                    } else {
                        this.setPagePosition(targetXY);
                    }
                }
            } else {
                this.hide();
                this.lastActive = new Date(0);
                this.onTargetOver(e);
            }
        } else if (!this.closable && this.isVisible()) {
            this.hide();
        }
    }
}); 


 
Ext.ns('Sch.util');

 
Sch.util.headerRenderers = function() {
    var dayTemplate = new Ext.XTemplate(
        '<table class="sch-nested-hdr-tbl" cellpadding="0" cellspacing="0"><tr>' + 
            '<tpl for="."><td class="sch-dayheadercell-{dayOfWeek}">{text}</td></tpl>' + 
        '</tr></table>'
    ).compile();
    
    var cellTemplate = new Ext.XTemplate(
        '<table class="sch-nested-hdr-tbl" cellpadding="0" cellspacing="0"><tr>' + 
            '<tpl for="."><td>{text}</td></tpl>' + 
        '</tr></table>'
    ).compile();

    return {
        quarterMinute : function(start, end, cfg, i) {
            cfg.headerCls = 'sch-timeheader sch-nested-hdr-pad';
            return '<table class="sch-nested-hdr-tbl" cellpadding="0" cellspacing="0"><tr><td>00</td><td>15</td><td>30</td><td>45</td></tr></table>';
        },

        dateCells : function(unit, increment, format) {
            
            return function(start, end, cfg) {
                cfg.headerCls = 'sch-timeheader sch-nested-hdr-nopad';
                
                var vals = [],
                    dt = start.clone();
                
                while(dt < end) {
                    vals.push({
                        text : dt.format(format)
                    });
                    dt = Sch.util.Date.add(dt, unit, increment);
                }
                
                return cellTemplate.apply(vals);
            };
        },

        dateNumber : function(start, end, cfg) {
            cfg.headerCls = 'sch-timeheader sch-nested-hdr-nopad';
            var vals = [],
                dt = start.clone();
                
            while(dt < end) {
                vals.push({
                    dayOfWeek : dt.getDay(),
                    text : dt.getDate()
                });
                dt = Sch.util.Date.add(dt, Sch.util.Date.DAY, 1);
            }
                
            return dayTemplate.apply(vals);
        },

        dayLetter : function(start, end, cfg) {
            cfg.headerCls = 'sch-timeheader sch-nested-hdr-nopad';
            var vals = [],
                dt = start;
                
            while(dt < end) {
                vals.push({
                    dayOfWeek : dt.getDay(),
                    text : Date.dayNames[dt.getDay()].substr(0, 1)
                });
                dt = Sch.util.Date.add(dt, Sch.util.Date.DAY, 1);
            }
            
            return dayTemplate.apply(vals);
        },

        dayStartEndHours : function(start, end, cfg) {
            cfg.headerCls = 'sch-timeheader sch-hdr-startend';
            return String.format('<span class="sch-hdr-start">{0}</span><span class="sch-hdr-end">{1}</span>', start.format('G'), end.format('G'));            
        }
    };
}();


 
Ext.ns('Sch');


Sch.ViewPreset = Ext.extend(Object, {
    constructor : function(config) {
        return config;
    }

    

    
    
    
    
    
    
    
    
    

    
});



Sch.ViewPresetHeaderRow = Ext.extend(Object, {
    constructor : function(config) {
        return config;
    }

    

    

    

    

    

    
});



 
Ext.ns('Sch');

Sch.BasicViewPresets = {
    hourAndDay : {
        timeColumnWidth : 40,   // Time column width, only applicable when locked columns are used
        displayDateFormat : 'G:i',  // Controls how dates will be displayed in tooltips etc
        shiftIncrement : 1,     // Controls how much time to skip when calling shiftNext and shiftPrevious.
        shiftUnit : "DAY",      // Valid values are "MILLI", "SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "QUARTER", "YEAR".
        defaultSpan : 12,       // By default, if no end date is supplied to a view it will show 12 hours
        timeResolution : {      // Dates will be snapped to this resolution
            unit : "MINUTE",    // Valid values are "MILLI", "SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "QUARTER", "YEAR".
            increment : 15
        },
        headerConfig : {    // This defines your header, you must include a "middle" object, and top/bottom are optional. For each row you can define "unit", "increment", "dateFormat", "renderer", "align", and "scope"
            middle : {              
                unit : "HOUR",
                dateFormat : 'G:i'
            },
            top : {
                unit : "DAY",
                dateFormat : 'D d/m'
            }
        }
    },
    dayAndWeek : {
        timeColumnWidth : 100,
        displayDateFormat : 'Y-m-d G:i',
        shiftUnit : "DAY",
        shiftIncrement : 1,
        defaultSpan : 5,       // By default, show 5 days
        timeResolution : {
            unit : "HOUR",
            increment : 1
        },
        headerConfig : {
            middle : {
                unit : "DAY",
                dateFormat : 'D d M'
            },
            top : {
                unit : "WEEK",
                renderer : function(start, end, cfg) {
                    return 'w.' + start.format('W M Y');
                }
            }
        }
    },

    weekAndDay : {
        timeColumnWidth : 100,
        displayDateFormat : 'Y-m-d',
        shiftUnit : "WEEK",
        shiftIncrement : 1,
        defaultSpan : 1,       // By default, show 1 week
        timeResolution : {
            unit : "DAY",
            increment : 1
        },
        headerConfig : {
             bottom : {
                unit : "DAY",
                increment : 1,
                dateFormat : 'd/m'
            },
            middle : {
                unit : "WEEK",
                dateFormat : 'D d M',
                align : 'left'
            }
        }
    },

    weekAndMonth : {
        timeColumnWidth : 100,
        displayDateFormat : 'Y-m-d',
        shiftUnit : "WEEK",
        shiftIncrement : 5,
        defaultSpan : 6,       // By default, show 6 weeks
        timeResolution : {
            unit : "DAY",
            increment : 1
        },
        headerConfig : {
            middle : {
                unit : "WEEK",
                renderer : function(start, end, cfg) {
                    cfg.align = 'left';
                    return start.format('d M');
                }
            },
            top : {
                unit : "MONTH",
                dateFormat : 'M Y'
            }
        }
    },

    monthAndYear : {
        timeColumnWidth : 110,
        displayDateFormat : 'Y-m-d',
        shiftIncrement : 3,
        shiftUnit : "MONTH",
        defaultSpan : 12,       // By default, show 12 months
        timeResolution : {
            unit : "DAY",
            increment : 1
        },
        headerConfig : {
            middle : {
                unit : "MONTH",
                dateFormat : 'M Y'
            },
            top : {
                unit : "YEAR",
                dateFormat : 'Y'
            }
        }
    },
    year : {
        timeColumnWidth : 100,
        displayDateFormat : 'Y-m-d',
        shiftUnit : "YEAR",
        shiftIncrement : 1,
        defaultSpan : 1,       // By default, show 1 year
        timeResolution : {
            unit : "MONTH",
            increment : 1
        },
        headerConfig : {
            bottom : {
                unit : "QUARTER",
                renderer : function(start, end, cfg) {
                    return String.format('Q{0}', Math.floor(start.getMonth() / 3) + 1);
                }
            },
            middle : {
                unit : "YEAR",
                dateFormat : 'Y'
            }
        }
    }
};

// Register all the basic presets with the Preset Manager
(function() {
    var pm = Sch.PresetManager,
        bvp = Sch.BasicViewPresets;

    for (var p in bvp) {
        if (bvp.hasOwnProperty(p)) {
            pm.registerPreset(p, bvp[p]);
        }
    }
})();

 
Ext.ns('Sch');


Sch.AbstractSchedulerPanel = Ext.extend(Ext.grid.EditorGridPanel, {
    
    resizeHandles : 'end',
    
    
    weekStartDay : 1,

    
    snapToIncrement : false,

    
    readOnly : false,

    
    
    
    
    
    viewPreset : 'weekAndDay',

    // The width of the left + right border of your event, needed to calculate the correct start/end positions
    eventBorderWidth : 2,
    enableColLock : false,
    trackMouseOver : false,
    columnLines : true,
    
    // protected
    cmpCls : undefined, // Must be set by subclass
    
    // protected
    eventSelector : undefined, // Must be set by subclass
    
    
    overClass : 'sch-event-hover',
    
    // private
    resizeHandleHtml : '<div class="x-resizable-handle sch-resizable-handle-{0}"></div>',
    
    
    setReadOnly : function(readOnly) {
        this.readOnly = readOnly;
        this.el[readOnly ? 'addClass' : 'removeClass'](this.cmpCls + '-readonly');
    },

    
    setTimeColumnWidth : function(width) {
        this.timeColumnWidth = width;
        this.getView().updateTimeColumnWidths();
    },

    
    getEventRecordFromDomId : function(id) {
        throw 'Abstract method call';
    },
    
    
    getEventIdFromDomNodeId : function(id) {
        return id.substring(this.eventPrefix.length);
    },
    
    
    getXYFromDate : function(date) {
        return this.view.getXYFromDate(date);
    },

    
    getDateFromDomEvent : function(e, roundingMethod) {
        return this.getDateFromXY(e.getXY(), roundingMethod);
    },
    
    
    getDateFromX : function(x, roundingMethod) {   
        return this.getDateFromXY([x,0], roundingMethod);
    },

    
    getDateFromXY : function(xy, roundingMethod) {
        var date = this.view.getDateFromXY(xy);

        if (date && roundingMethod) {
            date = this[roundingMethod + 'Date'](date);
        }
        
        return date;
    },
    
    //private
    roundDate : function(date) {
        return this.timeAxis.roundDate(date);
    },
    
    //private
    floorDate : function(date) {
        return this.timeAxis.floorDate(date);
    },

    //private
    ceilDate : function(date) {
        return this.timeAxis.ceilDate(Date);
    },
    
    
    getFormattedDate : function(date) {
        return date.format(this.getDisplayDateFormat());
    },
    
    
    getFormattedEndDate : function(endDate, startDate) {  
        var ta = this.timeAxis,
            resUnit = ta.getResolution().unit;

        // If resolution is day or greater, and end date is greater then start date
        if (resUnit in this.largeUnits && endDate.getHours() === 0 && endDate.getMinutes() === 0 && 
            !(endDate.getYear() === startDate.getYear() && endDate.getMonth() === startDate.getMonth() && endDate.getDate() === startDate.getDate())) {
            endDate = Sch.util.Date.add(endDate, Date.DAY, -1);
        }
        return endDate.format(this.getDisplayDateFormat());
    },
    
    
    getTimeResolution : function() {
        return this.timeAxis.getResolution();
    },

    
    setTimeResolution : function(unit, increment) {
        this.timeAxis.setResolution(unit, increment);

        // View will have to be updated to support snap to increment
        if (this.snapToIncrement) {
            this.getView().refresh(true);
        }
    },
    
    // private, clean up
    onDestroy : function() {
        if (this.tip) {
            this.tip.destroy();
        }
        Sch.AbstractSchedulerPanel.superclass.onDestroy.call(this);
    },
    
    // private
    afterRender : function() {
        Sch.AbstractSchedulerPanel.superclass.afterRender.apply(this, arguments);
       
        this.el.addClass(this.cmpCls);
        
        if (this.readOnly) {
            this.el.addClass(this.cmpCls + '-readonly');
        }

        var view = this.getView();
        this.relayEvents(view, ['timeheaderdblclick']);
        
        if(this.overClass){
            this.mon(this.getView().mainBody, {
                "mouseover": this.onMouseOver,
                "mouseout": this.onMouseOut,
                scope:this
            });
        }
        
        if (this.tooltipTpl) {
            this.setupTooltip();
        }

        this.setupScheduleEvents();
    },

    setupScheduleEvents : function() {
        this.mon(this.getView().mainBody, {
            click : function(e) {
                var t = e.getTarget('td.sch-timetd', 2);
                if (t) {
                    var clickedDate = this.getDateFromDomEvent(e, 'floor');
                    this.fireEvent('scheduleclick', this, clickedDate, this.view.findRowIndex(t), e);
                }
            },
            dblclick : function(e) {
                var t = e.getTarget('td.sch-timetd', 2);
                if (t) {
                    var clickedDate = this.getDateFromDomEvent(e, 'floor');
                    this.fireEvent('scheduledblclick', this, clickedDate, this.view.findRowIndex(t), e);
                }
            },
            contextmenu : function(e) {
                var t = e.getTarget('td.sch-timetd', 2);
                if (t) {
                    var clickedDate = this.getDateFromDomEvent(e, 'floor');
                    this.fireEvent('schedulecontextmenu', this, clickedDate, this.view.findRowIndex(t), e);
                }
            },
            scope : this
        }, this);
    },
   
    // private
    switchViewPreset : function(preset, startDate, endDate, initial) {
        if (this.fireEvent('beforeviewchange', this) !== false) {
            if (Ext.isString(preset)) {
                preset = Sch.PresetManager.getPreset(preset);
            }

            if (!preset) {
                throw 'View preset not found';
            }
           
            // Cache all properties
            Ext.apply(this, preset);
              
            var timeAxisCfg = {
                unit : this.getSmallestHeaderUnit(),
                increment : this.getSmallestHeaderIncrement(),
                resolutionUnit : this.timeResolution.unit, 
                resolutionIncrement : this.timeResolution.increment,
                weekStartDay : this.weekStartDay,
                mainUnit : this.getTimelineUnit(),
                defaultSpan : this.defaultSpan || 1
            };
            
            if (initial) {
                if (!this.timeAxis) {
                    this.timeAxis = new Sch.TimeAxis();
                } 
                timeAxisCfg.start = startDate || new Date();
                timeAxisCfg.end = endDate;

                this.timeAxis.on('reconfigure', this.onTimeAxisReconfigure, this);
            } else {
                timeAxisCfg.start = startDate || this.timeAxis.getStart();
                timeAxisCfg.end = endDate;
            }

            this.timeAxis.reconfigure(timeAxisCfg);
        }
    },

    // private
    onTimeAxisReconfigure : function() {
        this.fireEvent('viewchange', this);
    },

    // private
    getShiftIncrement : function() {
        return this.shiftIncrement || 1;
    },

    
    getStart : function() {
        return this.timeAxis.getStart();
    },
    
    
    getEnd : function() {
        return this.timeAxis.getEnd();
    },
    
    // private
    onMouseOver : function(e){
        var item = e.getTarget(this.eventSelector, this.view.cellSelectorDepth);
        if(item && item !== this.lastItem){
            this.lastItem = item;
            Ext.fly(item).addClass(this.overClass);
        }
    },
    
    // private
    onMouseOut : function(e){
        if(this.lastItem){
            if(!e.within(this.lastItem, true, true)){
                Ext.fly(this.lastItem).removeClass(this.overClass);
                delete this.lastItem;
            }
        }
    },
        
    
    setSnapEnabled : function(enabled) {
        this.snapToIncrement = enabled;

        if (enabled) {
            this.getView().refresh(true);
        }
    },
    
    // private
    initComponent : function() {

        if (!this.viewPreset) {
            throw 'You must define a valid view preset object. See sch.basicviewpresets.js for reference';
        }
        
        this.switchViewPreset(this.viewPreset, this.startDate, this.endDate, true);

        var largeUnits = {},
            D = Sch.util.Date;
        
        largeUnits[D.DAY] = null;
        largeUnits[D.WEEK] = null;
        largeUnits[D.MONTH] = null;
        largeUnits[D.QUARTER] = null;
        largeUnits[D.YEAR] = null;

        Ext.applyIf(this, {
            eventPrefix : this.id + '-ev-',
            largeUnits : largeUnits
        });
        
        if(Ext.isArray(this.columns) && !this.colModel){
            if (this.columns.length > 0 && this.columns[0].locked) {
                this.colModel = new Ext.ux.grid.LockingColumnModel(this.columns);
                delete this.columns;
            }
        }

        if(!this.columns && !this.colModel){
            // No columns specified at all, fall back to empty array
            this.columns = [];
        }

        Sch.AbstractSchedulerPanel.superclass.initComponent.call(this);
        
        // HACK: Inline override that hijacks the "setEditor" method of each configured column to prevent the editorgridpanel from destroying the editors across
        // column config changes (which happens when changing scheduling view)
        Ext.each(this.getColumnModel().config, function(col) { col.setEditor = Ext.emptyFn; });
    },
    
    
    tipCfg : {
        cls : 'sch-tip',
        showDelay:1000,
        autoHide : true,
        anchor : 'b'
    },

    // private
    setupTooltip : function() {
        var v = this.getView(),
            tipCfg = Ext.apply({
                renderTo: Ext.getBody(),
                delegate: this.eventSelector,
                target: v.mainBody,
                listeners: {
                    beforeshow : {
                        fn : function (tip) {
                            if (!tip.triggerElement || !tip.triggerElement.id) {
                                return false;
                            }
                            
                            var record = this.getTooltipRecord(tip.triggerElement);

                            if (!record || this.fireEvent('beforetooltipshow', this, record) === false) {
                                return false;
                            }

                            tip.update(this.tooltipTpl.apply(record.data));
                            
                            return true;
                        },
                        scope : this
                    }
                }
            }, this.tipCfg);
        
        this.tip = new Ext.ToolTip(tipCfg);
    },
    
    getTooltipRecord : function(el) {
        return this.getEventRecordFromDomId(el.id);
    },

    // private
    constructor : function(config) {
        this.addEvents(
            
            
            'beforetooltipshow',
            
            
            'beforeviewchange',
            
            
            'viewchange',
            
            
            'timeheaderdblclick',
            
            
            'scheduleclick',    

            
            'scheduledblclick',

            
            'schedulecontextmenu'
        );

        Sch.AbstractSchedulerPanel.superclass.constructor.call(this, config);
    },

    // private
    getShiftUnit: function() {
        return this.shiftUnit || this.getTimelineUnit();
    },

    // private
    getShiftIncrement : function() {
        return this.shiftIncrement || 1;
    },

    // private
    getTimeColumnWidth : function() {
        return this.timeColumnWidth || 100;
    },

    // private
    getDisplayDateFormat : function() {
        return this.displayDateFormat;
    },

    // private
    getHeaderConfig : function() {
        return this.headerConfig;
    },

    // private
    getTimelineUnit : function() {
        return this.headerConfig.middle.unit;
    },

    // private
    getTimelineIncrement : function() {
        return this.headerConfig.middle.increment || 1;
    },

    // private
    getSmallestHeaderUnit : function() {
        return this.headerConfig.bottom ? this.headerConfig.bottom.unit : this.headerConfig.middle.unit;
    },

    // private
    getSmallestHeaderIncrement : function() {
        return (this.headerConfig.bottom ? this.headerConfig.bottom.increment : this.headerConfig.middle.increment) || 1;
    },

    
    shiftNext : function(amount) {
        amount = amount || this.getShiftIncrement();
        var unit = this.getShiftUnit();
        this.setTimeSpan(Sch.util.Date.add(this.getStart(), unit, amount), Sch.util.Date.add(this.getEnd(),unit, amount));
    },

    
    shiftPrevious : function(amount) {
        amount = -(amount || this.getShiftIncrement());
        var unit = this.getShiftUnit();
        this.setTimeSpan(Sch.util.Date.add(this.getStart(), unit, amount), Sch.util.Date.add(this.getEnd(), unit, amount));
    },

    
    goToNow : function() {
        this.setTimeSpan(new Date());
    },

    
    setTimeSpan : function(start, end) {
        if (this.timeAxis) {
            this.timeAxis.setTimeSpan(start, end);
        }
    },

    
    setStart : function(date) {
        this.setTimeSpan(date);
    },

    
    setEnd : function(date) {
        this.setTimeSpan(null, date);
    },

    
    getTimeAxis : function() {
        return this.timeAxis;
    }
});


